Serialisierung
Unter Serialisierung versteht man die Umwandlung von Datentypen in ein Format, das z. B. für den Transport oder die Speicherung geeignet ist. Die Deserialisierung ist der Prozess, der dies wieder rückgängig macht. Dies geschieht verlustfrei, d. h. Daten können in ein Serialisierungsziel und aus diesem heraus konvertiert werden, ohne dass Datentypinformationen oder die Daten selbst verloren gehen.
In JavaScript erfolgt die Serialisierung normalerweise zwischen JavaScript-Objekten und JSON. JSON unterstützt nur String, Number, Boolean, Objects und Arrays. JavaScript hingegen unterstützt viele andere Typen wie BigInt, ArrayBuffer, typisierte Arrays, Date, benutzerdefinierte Klasseninstanzen und viele mehr. Um nun JavaScript-Daten mit JSON an einen Server zu übertragen, benötigen Sie einen Serialisierungsprozess (auf dem Client) und einen Deserialisierungsprozess (auf dem Server), oder umgekehrt, wenn der Server Daten als JSON an den Client sendet. Die Verwendung von JSON.parse
und JSON.stringify
ist dafür oft nicht ausreichend, da sie nicht verlustfrei ist.
Dieser Serialisierungsprozess ist für nicht-triviale Daten absolut notwendig, da JSON selbst bei einfachen Typen wie einem Datum seine Informationen verliert. Ein neues Datum wird schließlich als String in JSON serialisiert:
const json = JSON.stringify(new Date);
//'"2022-05-13T20:48:51.025Z"
Wie Sie sehen können, ist das Ergebnis von JSON.stringify ein JSON-String. Wenn Sie es mit JSON.parse wieder deserialisieren, erhalten Sie kein Datumsobjekt, sondern einen String.
const value = JSON.parse('"2022-05-13T20:48:51.025Z"');
//"2022-05-13T20:48:51.025Z"
Zwar gibt es verschiedene Workarounds, um JSON.parse die Deserialisierung von Datumsobjekten beizubringen, doch sind diese fehleranfällig und wenig leistungsfähig. Um eine typsichere Serialisierung und Deserialisierung für diesen Fall und viele andere Typen zu ermöglichen, ist ein Serialisierungsprozess erforderlich.
Es sind vier Hauptfunktionen verfügbar: serialize
, cast
, deserialize
und validatedDeserialize
. Unter der Haube dieser Funktionen wird standardmäßig der global verfügbare JSON-Serialisierer von @deepkit/type
verwendet, es kann aber auch ein benutzerdefiniertes Serialisierungsziel verwendet werden.
Deepkit Type unterstützt benutzerdefinierte Serialisierungsziele, verfügt aber bereits über ein leistungsfähiges JSON-Serialisierungsziel, das Daten als JSON-Objekte serialisiert und dann mit JSON.stringify korrekt und sicher in JSON umgewandelt werden kann. Mit @deepkit/bson
kann auch BSON als Serialisierungsziel verwendet werden. Wie man ein benutzerdefiniertes Serialisierungsziel (z.B. für einen Datenbanktreiber) erstellt, erfahren Sie im Abschnitt Benutzerdefinierter Serialisierer.
Beachten Sie, dass die Serialisierer aus Kompatibilitätsgründen zwar auch Daten validieren, diese Validierungen sich aber von der Validierung in Validation unterscheiden. Nur die Funktion cast
ruft nach erfolgreicher Deserialisierung auch den vollständigen Validierungsprozess aus dem Kapitel Validation auf und gibt einen Fehler aus, wenn die Daten nicht gültig sind.
Alternativ kann validatedDeserialize
verwendet werden, um nach der Deserialisierung zu validieren. Eine weitere Alternative ist der manuelle Aufruf der Funktionen validate
oder validates
für deserialisierte Daten aus der Funktion deserialize
, siehe Validation.
Alle Funktionen aus Serialisierung und Validierung werfen bei Fehlern einen ValidationError
aus @deepkit/type
.
Serialisierung
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);
Die Funktion serialize
konvertiert die übergebenen Daten standardmäßig mit dem JSON-Serializer in ein JSON-Objekt, d.h.: String, Number, Boolean, Object, oder Array. Das Ergebnis kann dann mit JSON.stringify
sicher in ein JSON konvertiert werden.
Deserialisierung
Die Funktion deserialize
konvertiert die übergebenen Daten standardmäßig mit dem JSON-Serializer in die entsprechenden angegebenen Typen. Der JSON-Serializer erwartet ein JSON-Objekt, d.h.: String, Zahl, Boolean, Objekt oder Array. Dieses wird in der Regel durch einen JSON.parse
-Aufruf gewonnen.
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));
Wenn bereits der richtige Datentyp übergeben wird (z.B. ein Datumsobjekt im Fall von created
), dann wird dieser so übernommen, wie er ist.
Nicht nur eine Klasse, sondern jeder TypeScript-Typ kann als erstes Typ-Argument angegeben werden. Es können also auch Primitive oder sehr komplexe Typen übergeben werden:
deserialize<Date>('Sat Oct 13 2018 14:17:35 GMT+0200');
deserialize<string | number>(23);
Soft Type Konvertierung
Bei der Deserialisierung wird eine weiche Typkonvertierung durchgeführt. Das bedeutet, dass String und Number für String-Typen oder eine Number für einen String-Typ akzeptiert und automatisch konvertiert werden können. Dies ist z.B. dann sinnvoll, wenn Daten über eine URL angenommen und an den Deserialisierer übergeben werden. Da es sich bei der URL immer um einen String handelt, versucht Deepkit Type weiterhin, die Typen Number und Boolean aufzulösen.
deserialize<boolean>('false')); //false
deserialize<boolean>('0')); //false
deserialize<boolean>('1')); //true
deserialize<number>('1')); //1
deserialize<string>(1)); //'1'
Die folgenden weichen Typkonvertierungen sind in den JSON-Serialisierer integriert:
-
Zahl|Bigint: Number oder Bigint akzeptieren String, Number und BigInt.
parseFloat
oderBigInt(x)
werden im Falle einer notwendigen Umwandlung verwendet. -
Boolesch: Boolean akzeptiert Zahl und String. 0, '0', 'false' wird als
false
interpretiert. 1, '1', 'wahr' wird als 'wahr' interpretiert. -
Zeichenfolge: String akzeptiert Number, String, Boolean und viele mehr. Alle Nicht-String-Werte werden automatisch mit
String(x)
konvertiert.
Die weiche Umwandlung kann auch deaktiviert werden:
const result = deserialize(data, {loosely: false});
Bei ungültigen Daten wird nicht versucht, sie zu konvertieren, sondern es wird eine Fehlermeldung ausgegeben.
Benutzerdefinierter Serialisierer
Standardmäßig wird @deepkit/type
mit einem JSON-Serialisierer und einer Typ-Validierung für TypeScript-Typen geliefert. Sie können dies erweitern und die Serialisierungsfunktionalität hinzufügen oder entfernen oder die Art der Validierung ändern, da die Validierung auch mit dem Serialisierer verknüpft ist.
Neuer Serialisierer
Ein Serializer ist einfach eine Instanz der Klasse Serializer
mit registrierten Serializer-Vorlagen. Serializer-Vorlagen sind kleine Funktionen, die JavaScript-Code für den JIT-Serializer-Prozess erzeugen. Für jeden Typ (String, Number, Boolean usw.) gibt es eine eigene Serializer-Vorlage, die für die Rückgabe von Code zur Datenkonvertierung oder -validierung zuständig ist. Dieser Code muss mit der JavaScript-Engine kompatibel sein, die der Benutzer verwendet.
Nur während der Ausführung der Compiler-Vorlagenfunktion haben Sie (oder sollten Sie) vollen Zugriff auf den vollständigen Typ. Die Idee ist, dass Sie alle Informationen, die für die Umwandlung eines Typs erforderlich sind, direkt in den JavaScript-Code einbetten sollten, was zu hoch optimiertem Code führt (auch JIT-optimierter Code genannt).
Im folgenden Beispiel wird ein leerer Serialisierer erstellt.
import { EmptySerializer } from '@deepkit/type';
class User {
name: string = '';
created: Date = new Date;
}
const mySerializer = new EmptySerializer('mySerializer');
const user = deserialize<User>({ name: 'Peter', created: 0 }, undefined, mySerializer);
console.log(user);
$ ts-node app.ts
User { name: 'Peter', created: 0 }
Wie Sie sehen können, wurde nichts umgewandelt (created
ist immer noch eine Zahl, aber wir haben sie als date
definiert). Um dies zu ändern, fügen wir eine Serializer-Vorlage für die Deserialisierung des Typs Date hinzu.
mySerializer.deserializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`new Date(${state.accessor})`);
});
const user = deserialize<User>({ name: 'Peter', created: 0 }, undefined, mySerializer);
console.log(user);
$ ts-node app.ts
User { name: 'Peter', created: 2021-06-10T19:34:27.301Z }
Jetzt wandelt unser Serializer den Wert in ein Date-Objekt um.
Um das Gleiche für die Serialisierung zu tun, registrieren wir eine weitere Serialisierungsvorlage.
mySerializer.serializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`${state.accessor}.toJSON()`);
});
const user1 = new User();
user1.name = 'Peter';
user1.created = new Date('2021-06-10T19:34:27.301Z');
console.log(serialize(user1, undefined, mySerializer));
{ name: 'Peter', created: '2021-06-10T19:34:27.301Z' }
Unser neuer Serialisierer konvertiert nun das Datum aus dem Date-Objekt korrekt in eine Zeichenkette während des Serialisierungsprozesses.
Beispiele
Um viele weitere Beispiele zu sehen, können Sie einen Blick auf den Code des Links:https://github.com/deepkit/deepkit-framework/blob/master/packages/type/src/serializer.ts#L1688[JSON-Serializer] werfen, der in Deepkit Type enthalten ist.
Einen Serialisierer Erweitern
Wenn Sie einen vorhandenen Serialisierer erweitern möchten, können Sie dies über Klassenvererbung tun. Dies funktioniert, weil Serialisierer so geschrieben werden sollten, dass sie ihre Vorlagen im Konstruktor registrieren.
class MySerializer extends Serializer {
constructor(name: string = 'mySerializer') {
super(name);
this.registerTemplates();
}
protected registerTemplates() {
this.deserializeRegistry.register(ReflectionKind.string, (type, state) => {
state.addSetter(`String(${state.accessor})`);
});
this.deserializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`new Date(${state.accessor})`);
});
this.serializeRegistry.registerClass(Date, (type, state) => {
state.addSetter(`${state.accessor}.toJSON()`);
});
}
}
const mySerializer = new MySerializer();