Serialisation
Serialization is the process of converting data types into a format suitable for transport or storage, for example. Deserialization is the process of undoing this. This is done loss-lessly, meaning that data can be converted to and from a serialization target without losing data type information or the data itself.
In JavaScript, serialization is usually between JavaScript objects and JSON. JSON supports only String, Number, Boolean, Objects, and Arrays. JavaScript, on the other hand, supports many other types such as BigInt, ArrayBuffer, typed arrays, Date, custom class instances, and many more. Now, to transmit JavaScript data to a server using JSON, you need a serialization process (on the client) and a deserialization process (on the server), or vice versa if the server sends data to the client as JSON. Using JSON.parse
and JSON.stringify
is often not sufficient for this, as it is not lossless.
This serialization process is absolutely necessary for non-trivial data, since JSON loses its information even for basic types like a date. A new Date is finally serialized as a string in JSON:
const json = JSON.stringify(new Date); //'"2022-05-13T20:48:51.025Z"
As you can see, the result of JSON.stringify is a JSON string. If you deserialize it again with JSON.parse, you will not get a date object, but a string.
const value = JSON.parse('"2022-05-13T20:48:51.025Z"'); //"2022-05-13T20:48:51.025Z"
Although there are various workarounds to teach JSON.parse to deserialize Date objects, they are error-prone and poorly performing. To enable typesafe serialization and deserialization for this case and many other types, a serialization process is necessary.
There are four main functions available: serialize
, cast
, deserialize
and validatedDeserialize
. Under the hood of these functions, the globally available JSON serializer from @deepkit/type
is used by default, but a custom serialization target can also be used.
Deepkit Type supports user-defined serialization targets, but already comes with a powerful JSON serialization target that serializes data as JSON objects and then can be correctly and safely converted as JSON using JSON.stringify. With @deepkit/bson
, BSON can also be used as a serialization target. How to create a custom serialization target (for example for a database driver) can be learned in the Custom Serializer section.
Note that although serializers also validate data for compatibility, these validations are different from the validation in Validation. Only the cast
function also calls the full validation process from the Validation chapter after successful deserialization, and throws an error if the data is not valid.
Alternatively, validatedDeserialize
can be used to validate after deserialization. Another alternative is to manually call the validate
or validates
functions on deserialized data from the deserialize
function, see Validation.
All functions from serialization and validation throw a ValidationError
from @deepkit/type
on errors.
Cast
The cast
function expects as first type argument a TypeScript type, and as second argument the data to cast. The data is casted to the given type, and if successful, the data is returned. If the data is not compatible with the given type and can not be converted automatically, a ValidationError
is thrown.
import { cast } from '@deepkit/type'; cast<string>(123); //'123' cast<number>('123'); //123 cast<number>('asdasd'); // throws ValidationError cast<string | number>(123); //123
class MyModel { id: number = 0; created: Date = new Date; constructor(public name: string) { } } const myModel = cast<MyModel>({ id: 5, created: 'Sat Oct 13 2018 14:17:35 GMT+0200', name: 'Peter', });
The deserialize
function is similar to cast
, but does not throw an error if the data is not compatible with the given type. Instead, the data is converted as far as possible and the result is returned. If the data is not compatible with the given type, the data is returned as it is.
Serialization
import { serialize } from '@deepkit/type'; class MyModel { id: number = 0; created: Date = new Date; constructor(public name: string) { } } const model = new MyModel('Peter'); const jsonObject = serialize<MyModel>(model); //{ // id: 0, // created: '2021-06-10T15:07:24.292Z', // name: 'Peter' //} const json = JSON.stringify(jsonObject);
The function serialize
converts the passed data by default with the JSON serializer into a JSON object, that is: String, Number, Boolean, Object, or Array. The result of this can then be safely converted to a JSON using JSON.stringify
.
Deserialiazation
The function deserialize
converts the passed data per default with the JSON serializer into the corresponding specified types. The JSON serializer expects a JSON object, i.e.: string, number, boolean, object, or array. This is usually obtained from a JSON.parse
call.
import { deserialize } from '@deepkit/type'; class MyModel { id: number = 0; created: Date = new Date; constructor(public name: string) { } } const myModel = deserialize<MyModel>({ id: 5, created: 'Sat Oct 13 2018 14:17:35 GMT+0200', name: 'Peter', }); //from JSON const json = '{"id": 5, "created": "Sat Oct 13 2018 14:17:35 GMT+0200", "name": "Peter"}'; const myModel = deserialize<MyModel>(JSON.parse(json));
If the correct data type is already passed (for example, a Date object in the case of created
), then this is taken as it is.
Not only a class, but any TypeScript type can be specified as the first type argument. So even primitives or very complex types can be passed:
deserialize<Date>('Sat Oct 13 2018 14:17:35 GMT+0200'); deserialize<string | number>(23);
Soft Type Conversion
In the deserialization process a soft type conversion is implemented. This means that String and Number for String types or a Number for a String type can be accepted and converted automatically. This is useful, for example, when data is accepted via a URL and passed to the deserializer. Since the URL is always a string, Deepkit Type still tries to resolve the types for Number and Boolean.
deserialize<boolean>('false')); //false deserialize<boolean>('0')); //false deserialize<boolean>('1')); //true deserialize<number>('1')); //1 deserialize<string>(1)); //'1'
The following soft type conversions are built into the JSON serializer:
- number|bigint: Number or Bigint accept String, Number, and BigInt.
parseFloat
orBigInt(x)
are used in case of a necessary conversion. - boolean: Boolean accepts Number and String. 0, '0', 'false' is interpreted as
false
. 1, '1', 'true' is interpreted astrue
. - string: String accepts Number, String, Boolean, and many more. All non-string values are automatically converted with
String(x)
.
The soft conversion can also be deactivated:
const result = deserialize(data, {loosely: false});
In the case of invalid data, no attempt is made to convert it and instead an error message is thrown.