- Transformation from a Class instance to a plain JavaScript object. (Serialization)
- Transformation from plain a JavaScript object to a Class instance. (Deserialization)
- Designed for TypeScript project, using TypeScript decorators.
- DataObject always takes whitelist approach. It means that;
- Only
@property
decorated property will be output (serialized) into plain JavaScript object. - Only
@property
decorated property will be taken (deserialized) into class instance.
- Only
- Currently, Class inheritance is not supported.
class User {
@property()
@required()
userId!: string;
@property()
name: string = '';
@property()
postalAddress?: Address;
@property()
@context('factory', 'toPlain')
metadata: Record<string, unknown> = {};
@property()
tags: Set<string> = new Set();
static factory = createFactory(User);
static toPlain = createToPlain(User);
}
class Address {
@property()
@required()
country!: string;
@property()
@required()
@context('!public')
@validator(isPostalCode)
postalCode!: string;
static factory = createFactory(Address);
static toPlain = createToPlain(Address);
}
function isPostalCode(code: string) {
return /^[\d]{7}$/.test(code);
}
const source = {
userId: 'e7cebd38-9e3a-4487-9485-b3e3be03cd32',
name: 'test user',
postalAddress: {
country: 'jp',
postalCode: '1234567',
},
metadata: {
lastLogin: 1622940893174,
},
tags: ['loyal', 'active'],
};
// Transformation from plain object to class instance. (deserialization)
const created = User.factory(source);
// Transformation from class instance to plain object. (serialization)
const plain = User.toPlain(created, 'public');
To enable a Class to work as "DataObject", you should do first;
- Implement at least one
@property
decorated property. - Implement
factory
static method by usingcreateFactory
utility. - Implement
toPlain
static method by usingcreateToPlain
utility.
The simplest class looks like;
class Entity {
@property()
id?: string;
static factory = createFactory(Entity);
static toPlain = createToPlain(Entity);
}
DataObject will look up at types given through TypeScript type system for transformation. It is also possible to tell its type explicitly. Also, you can set your own transformer.
@property
name: string;
- In toPlain, value will be output as-is.
- In factory, input value will be type-coerced.
@property
code: number;
- In toPlain, value will be output as-is.
- In factory, input value will be type-coerced. If the result is
NaN
, Error will be thrown.
@property
active: boolean;
- In toPlain, value will be output as-is.
- In factory, input value will be type-coerced.
@property
active: CustomClass; // CustomClass is a "DataObject" which has factory and toPlain static methods.
- In toPlain, value will be transformed with using
CustomClass#toPlain()
. - In factory, value will be transformed with using
CustomClass#factory()
.
@property
list: string[];
- In toPlain, each value in array will be output as-is.
- In factory, each value in array will be taken as-is. (no type coercion)
- Other types are same.
@property({ type: () => CustomClass })
list: CustomClass[];
- Need to set
type
option to@property
decorator. - In toPlain, each value in array will be transformed with using
CustomClass#toPlain()
. A special attribute__type: CustomClass
will be added. - In factory, each value in array will be transformed with using
CustomClass#factory()
.
@property({ type: () => [CustomClass, AnotherCustomClass] })
list: Array<CustomClass | AnotherCustomClass>; // Union type also works
- Need to set
type
option to@property
decorator. - In toPlain, each value in array will be transformed with using
CustomClass#toPlain()
orAnotherCustomClass#toPlain()
. A special attribute__type: CustomClass
or__type: AnotherCustomClass
will be added. - In factory, each value in array will be transformed with using
CustomClass#factory()
orAnotherCustomClass#factory()
according to a special attribute__type
.
@property()
list: Set<string>;
@property({ type: () => CustomClass })
list: Set<CustomClass>;
- Same as array
@property
dict: Record<string, string>;
- In toPlain, each value in object will be output as-is.
- In factory, each value in object will be taken as-is. (no type coercion)
- Other type pairs (e.g.
Record<string, unknown>
) are same.
@property{ type: () => CustomClass }
dict: Map<string, CustomClass>;
- Need to set
type
options to@property
decorator. - In toPlain, each value in array will be transformed with using
CustomClass#toPlain()
. A special attribute__type: CustomClass
will be added. - In factory, each value in array will be transformed with using
CustomClass#factory()
.
@property{ type: () => CustomClass, isMap: true }
dict: Record<string, CustomClass>;
- Need to set
type
andisMap
options to@property
decorator if you want to use object like ES6 Map. - In toPlain, each value in array will be transformed with using
CustomClass#toPlain()
. A special attribute__type: CustomClass
will be added. - In factory, each value in array will be transformed with using
CustomClass#factory()
.
@property{ type: () => [CustomClass, AnotherCustomClass] }
dict: Map<string, CustomClass | AnotherCustomClass>;
- Union type works same as array of union types.
- If the value given to factory was
undefined
, the value is not taken in.
You can use your own transformer by setting transformer
option.
@property({ transformer: jsDateTransformer })
timestamp: Date = new Date();
Please check src/bundle/jsDateTransformer
for actual transformer implementation, which transformed JavaScript
Date
object to ISO date string and vice-versa.
In case a property has been decorated with @required
,
factory will check if the property really exists in given source.
@property()
@required()
id!: string;
Error will be thrown if it is missing.
You can make transformation work only in specific contexts.
- factory method has "factory" context as default.
- toPlain method has "toPlain" context as default.
class Entity {
@property()
@context('!response')
id: string;
@property('response')
get name(): string { ... }
}
Entity.toPlain(instance, 'response');
You can specify custom context to both toPlain and factory. Exclusion (heading !
) is available.
With above example, toPlain will output only name
into resulted object.
You can spread the value in toPlain
process with using @spread
decorator.
Also, you can give context option which works same as @context
.
class Entity {
@property()
id: string = 'my-id'
@property
@spread
details?: Record<string, unknown> = { item: 'value' }
}
Entity.toPlain(instance);
With above example, details
is spread, and the result should look like;
{ id: "my-id", item: "value" }
You can set validator function which is invoked in 'factory'.
Validator function should return true
or nothing (undefined
) in case of success.
In case of failure, it should return false or Error, or should throw Error.
If some validation failed, 'factory' will throw ValidationError
.
You can check what properties failed by checking the error thrown.
class Entity {
@property()
@validator((v: string) => v.length <= 4)
id?: string;
static factory = createFactory(Entity);
static toPlain = createToPlain(Entity);
}
try {
const entity = Entity.factory({ id: 'a_little_too_long' });
} catch (err) {
// err should be instance of ValidationError
// err.causes[0].key should be 'id'
// err.causes[0].error should be 'id validation failed'
}
Be noted the validator will be applied after value transformation finished, that means the argument validator takes is already transformed value.
Copyright (c) 2021 Keisuke Yamamoto