@tsed/json-mapper 8.0.0 → 8.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/package.json +8 -7
  2. package/src/components/DateMapper.spec.ts +43 -0
  3. package/src/components/DateMapper.ts +29 -0
  4. package/src/components/PrimitiveMapper.spec.ts +179 -0
  5. package/src/components/PrimitiveMapper.ts +64 -0
  6. package/src/components/SymbolMapper.spec.ts +23 -0
  7. package/src/components/SymbolMapper.ts +19 -0
  8. package/src/decorators/afterDeserialize.ts +39 -0
  9. package/src/decorators/beforeDeserialize.ts +39 -0
  10. package/src/decorators/jsonMapper.ts +16 -0
  11. package/src/decorators/onDeserialize.ts +29 -0
  12. package/src/decorators/onSerialize.ts +29 -0
  13. package/src/domain/JsonDeserializer.spec.ts +1437 -0
  14. package/src/domain/JsonDeserializer.ts +402 -0
  15. package/src/domain/JsonDeserializerOptions.ts +31 -0
  16. package/src/domain/JsonMapperCompiler.ts +247 -0
  17. package/src/domain/JsonMapperGlobalOptions.ts +14 -0
  18. package/src/domain/JsonMapperSettings.ts +7 -0
  19. package/src/domain/JsonMapperTypesContainer.ts +20 -0
  20. package/src/domain/JsonSerializer.spec.ts +1505 -0
  21. package/src/domain/JsonSerializer.ts +371 -0
  22. package/src/domain/JsonSerializerOptions.ts +17 -0
  23. package/src/domain/Writer.spec.ts +11 -0
  24. package/src/domain/Writer.ts +181 -0
  25. package/src/domain/__snapshots__/Writer.spec.ts.snap +10 -0
  26. package/src/hooks/alterAfterDeserialize.ts +5 -0
  27. package/src/hooks/alterBeforeDeserialize.ts +5 -0
  28. package/src/hooks/alterOnDeserialize.ts +5 -0
  29. package/src/hooks/alterOnSerialize.ts +5 -0
  30. package/src/index.ts +28 -0
  31. package/src/interfaces/JsonMapperMethods.ts +32 -0
  32. package/src/utils/deserialize.spec.ts +33 -0
  33. package/src/utils/deserialize.ts +12 -0
  34. package/src/utils/getObjectProperties.ts +5 -0
  35. package/src/utils/serialize.spec.ts +37 -0
  36. package/src/utils/serialize.ts +12 -0
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@tsed/json-mapper",
3
3
  "description": "Json mapper module for Ts.ED Framework",
4
4
  "type": "module",
5
- "version": "8.0.0",
5
+ "version": "8.0.2",
6
6
  "source": "./src/index.ts",
7
7
  "main": "./lib/esm/index.js",
8
8
  "module": "./lib/esm/index.js",
@@ -10,6 +10,7 @@
10
10
  "browser": "./lib/browser/json-mapper.umd.min.js",
11
11
  "exports": {
12
12
  ".": {
13
+ "@tsed/source": "./src/index.ts",
13
14
  "types": "./lib/types/index.d.ts",
14
15
  "browser": "./lib/browser/json-mapper.umd.min.js",
15
16
  "import": "./lib/esm/index.js",
@@ -41,18 +42,18 @@
41
42
  "tslib": "2.7.0"
42
43
  },
43
44
  "devDependencies": {
44
- "@tsed/barrels": "8.0.0",
45
- "@tsed/core": "8.0.0",
46
- "@tsed/schema": "8.0.0",
47
- "@tsed/typescript": "8.0.0",
45
+ "@tsed/barrels": "8.0.2",
46
+ "@tsed/core": "8.0.2",
47
+ "@tsed/schema": "8.0.2",
48
+ "@tsed/typescript": "8.0.2",
48
49
  "eslint": "9.12.0",
49
50
  "typescript": "5.4.5",
50
51
  "vitest": "2.1.2",
51
52
  "webpack": "^5.75.0"
52
53
  },
53
54
  "peerDependencies": {
54
- "@tsed/core": "8.0.0",
55
- "@tsed/schema": "8.0.0"
55
+ "@tsed/core": "8.0.2",
56
+ "@tsed/schema": "8.0.2"
56
57
  },
57
58
  "peerDependenciesMeta": {
58
59
  "@tsed/core": {
@@ -0,0 +1,43 @@
1
+ import {DateMapper} from "./DateMapper.js";
2
+
3
+ describe("DateMapper", () => {
4
+ describe("deserialize()", () => {
5
+ it("should return a Date when the data is a string", () => {
6
+ const date = new Date();
7
+ const mapper = new DateMapper();
8
+
9
+ const value = mapper.deserialize(date.toISOString());
10
+
11
+ expect(value).toEqual(date);
12
+ });
13
+
14
+ it("should return a Date when the data is a number", () => {
15
+ const date = new Date();
16
+ const mapper = new DateMapper();
17
+
18
+ const value = mapper.deserialize(date.getTime());
19
+
20
+ expect(value).toEqual(date);
21
+ });
22
+
23
+ it("should return value when the data is a boolean/null/undefined", () => {
24
+ const date = new Date();
25
+ const mapper = new DateMapper();
26
+
27
+ expect(mapper.deserialize(false)).toEqual(false);
28
+ expect(mapper.deserialize(true)).toEqual(true);
29
+ expect(mapper.deserialize(null)).toEqual(null);
30
+ expect(mapper.deserialize(undefined)).toBeUndefined();
31
+ });
32
+ });
33
+ describe("serialize()", () => {
34
+ it("should return value", () => {
35
+ const date = new Date();
36
+ const mapper = new DateMapper();
37
+
38
+ const value = mapper.serialize(date);
39
+
40
+ expect(value).toEqual(date.toISOString());
41
+ });
42
+ });
43
+ });
@@ -0,0 +1,29 @@
1
+ import {isBoolean} from "@tsed/core";
2
+
3
+ import {JsonMapper} from "../decorators/jsonMapper.js";
4
+ import {JsonMapperMethods} from "../interfaces/JsonMapperMethods.js";
5
+
6
+ /**
7
+ * Mapper for `Date` type.
8
+ * @jsonmapper
9
+ * @component
10
+ */
11
+ @JsonMapper(Date)
12
+ export class DateMapper implements JsonMapperMethods {
13
+ deserialize(data: string | number): Date;
14
+ deserialize(data: boolean | null | undefined): boolean | null | undefined;
15
+ deserialize(data: any): any {
16
+ // don't convert unexpected data. In normal case, Ajv reject unexpected data.
17
+ // But by default, we have to skip data deserialization and let user to apply
18
+ // the right mapping
19
+ if (isBoolean(data) || data === null || data === undefined) {
20
+ return data;
21
+ }
22
+
23
+ return new Date(data);
24
+ }
25
+
26
+ serialize(object: Date): any {
27
+ return object ? new Date(object).toISOString() : object;
28
+ }
29
+ }
@@ -0,0 +1,179 @@
1
+ import {catchError} from "@tsed/core";
2
+
3
+ import {PrimitiveMapper} from "./PrimitiveMapper.js";
4
+
5
+ describe("PrimitiveMapper", () => {
6
+ describe("deserialize()", () => {
7
+ it("should return value (number => string)", () => {
8
+ const mapper = new PrimitiveMapper();
9
+ const data = 1;
10
+ const ctx = {
11
+ type: String,
12
+ collectionType: undefined,
13
+ next: vi.fn()
14
+ };
15
+
16
+ const value = mapper.deserialize(data, ctx as never);
17
+
18
+ expect(value).toEqual("1");
19
+ });
20
+ it("should return value (string => string)", () => {
21
+ const mapper = new PrimitiveMapper();
22
+ const data = "1";
23
+ const ctx = {
24
+ type: String,
25
+ collectionType: undefined,
26
+ next: vi.fn()
27
+ };
28
+
29
+ const value = mapper.deserialize(data, ctx as never);
30
+
31
+ expect(value).toEqual("1");
32
+ });
33
+ it("should return value (null => number)", () => {
34
+ const mapper = new PrimitiveMapper();
35
+ const data = null;
36
+ const ctx = {
37
+ type: Number,
38
+ collectionType: undefined,
39
+ next: vi.fn()
40
+ };
41
+
42
+ const value = mapper.deserialize(data, ctx as never);
43
+
44
+ expect(value).toEqual(null);
45
+ });
46
+ it("should return value ('null' => number)", () => {
47
+ const mapper = new PrimitiveMapper();
48
+ const data = "null";
49
+ const ctx = {
50
+ type: Number,
51
+ collectionType: undefined,
52
+ next: vi.fn()
53
+ };
54
+
55
+ const value = mapper.deserialize(data, ctx as never);
56
+
57
+ expect(value).toEqual(null);
58
+ });
59
+ it("should return value (string => number)", () => {
60
+ const mapper = new PrimitiveMapper();
61
+ const data = "1";
62
+ const ctx = {
63
+ type: Number,
64
+ collectionType: undefined,
65
+ next: vi.fn()
66
+ };
67
+
68
+ const value = mapper.deserialize(data, ctx as never);
69
+
70
+ expect(value).toEqual(1);
71
+ });
72
+ it("should return value (number => number)", () => {
73
+ const mapper = new PrimitiveMapper();
74
+ const data = 1;
75
+ const ctx = {
76
+ type: Number,
77
+ collectionType: undefined,
78
+ next: vi.fn()
79
+ };
80
+
81
+ const value = mapper.deserialize(data, ctx as never);
82
+
83
+ expect(value).toEqual(1);
84
+ });
85
+ it("should return value (wrong number => number)", () => {
86
+ const mapper = new PrimitiveMapper();
87
+ const data = "t1";
88
+ const ctx = {
89
+ type: Number,
90
+ collectionType: undefined,
91
+ next: vi.fn()
92
+ };
93
+
94
+ let actualError: any = catchError(() => mapper.deserialize(data, ctx as never));
95
+
96
+ expect(actualError.message).toEqual("Cast error. Expression value is not a number.");
97
+ });
98
+ it("should return value (truthy => boolean)", () => {
99
+ const mapper = new PrimitiveMapper();
100
+ const ctx: any = {
101
+ type: Boolean,
102
+ collectionType: undefined,
103
+ next: vi.fn()
104
+ };
105
+
106
+ expect(mapper.deserialize(1, ctx)).toEqual(true);
107
+ expect(mapper.deserialize("1", ctx)).toEqual(true);
108
+ expect(mapper.deserialize("true", ctx)).toEqual(true);
109
+ expect(mapper.deserialize(true, ctx)).toEqual(true);
110
+ });
111
+ it("should return value (falsy => boolean)", () => {
112
+ const mapper = new PrimitiveMapper();
113
+ const ctx: any = {
114
+ type: Boolean,
115
+ collectionType: undefined,
116
+ next: vi.fn()
117
+ };
118
+
119
+ expect(mapper.deserialize(0, ctx)).toEqual(false);
120
+ expect(mapper.deserialize("0", ctx)).toEqual(false);
121
+ expect(mapper.deserialize("", ctx)).toEqual(false);
122
+ expect(mapper.deserialize("false", ctx)).toEqual(false);
123
+ expect(mapper.deserialize(false, ctx)).toEqual(false);
124
+ expect(mapper.deserialize(undefined, ctx)).toBeUndefined();
125
+ });
126
+ it("should return value (null => boolean)", () => {
127
+ const mapper = new PrimitiveMapper();
128
+ const ctx: any = {
129
+ type: Boolean,
130
+ collectionType: undefined,
131
+ next: vi.fn()
132
+ };
133
+
134
+ expect(mapper.deserialize(null, ctx)).toEqual(null);
135
+ expect(mapper.deserialize("null", ctx)).toEqual(null);
136
+ });
137
+ });
138
+ describe("serialize()", () => {
139
+ it("should return value (string to string)", () => {
140
+ const mapper = new PrimitiveMapper();
141
+
142
+ const value = mapper.serialize("1", {type: String} as any);
143
+
144
+ expect(value).toEqual("1");
145
+ });
146
+
147
+ it("should return value (string to number)", () => {
148
+ const mapper = new PrimitiveMapper();
149
+
150
+ const value = mapper.serialize("1", {type: Number} as any);
151
+
152
+ expect(value).toEqual(1);
153
+ });
154
+
155
+ it("should return value (object)", () => {
156
+ const mapper = new PrimitiveMapper();
157
+
158
+ // in this case it's probably intended to be an object (or an error but we can decide for the developer and we can broke the code)
159
+ // TODO: for the major version, we can return undefined or throw an error?
160
+ const value = mapper.serialize({"1": "1"} as any, {type: Number} as any);
161
+
162
+ expect(value).toEqual({"1": "1"});
163
+ });
164
+
165
+ it("should return value (null)", () => {
166
+ const mapper = new PrimitiveMapper();
167
+ const value = mapper.serialize(null as any, {type: Number} as any);
168
+
169
+ expect(value).toEqual(null);
170
+ });
171
+
172
+ it("should return value (undefined)", () => {
173
+ const mapper = new PrimitiveMapper();
174
+ const value = mapper.serialize(undefined as any, {type: Number} as any);
175
+
176
+ expect(value).toEqual(undefined);
177
+ });
178
+ });
179
+ });
@@ -0,0 +1,64 @@
1
+ import {nameOf} from "@tsed/core";
2
+
3
+ import {JsonMapper} from "../decorators/jsonMapper.js";
4
+ import {JsonMapperCtx, JsonMapperMethods} from "../interfaces/JsonMapperMethods.js";
5
+
6
+ function isNullish(data: any) {
7
+ return [null, "null"].includes(data);
8
+ }
9
+
10
+ export class CastError extends Error {
11
+ name = "CAST_ERROR";
12
+
13
+ constructor(message: string) {
14
+ super(`Cast error. ${message}`);
15
+ }
16
+ }
17
+
18
+ /**
19
+ * Mapper for the `String`, `Number`, `BigInt` and `Boolean` types.
20
+ * @jsonmapper
21
+ * @component
22
+ */
23
+ @JsonMapper(String, Number, Boolean, BigInt)
24
+ export class PrimitiveMapper implements JsonMapperMethods {
25
+ deserialize<T>(data: any, ctx: JsonMapperCtx): string | number | boolean | void | null | BigInt {
26
+ return (this as any)[nameOf(ctx.type)] ? (this as any)[nameOf(ctx.type)](data, ctx) : undefined;
27
+ }
28
+
29
+ serialize(object: string | number | boolean | BigInt, ctx: JsonMapperCtx): string | number | boolean | BigInt {
30
+ return (this as any)[nameOf(ctx?.type)] && typeof object !== "object" ? (this as any)[nameOf(ctx.type)](object, ctx) : object;
31
+ }
32
+
33
+ protected String(data: any) {
34
+ return data === null ? null : "" + data;
35
+ }
36
+
37
+ protected Boolean(data: any) {
38
+ if (["true", "1", true].includes(data)) return true;
39
+ if (["false", "0", false].includes(data)) return false;
40
+ if (isNullish(data)) return null;
41
+ if (data === undefined) return undefined;
42
+
43
+ return !!data;
44
+ }
45
+
46
+ protected Number(data: any) {
47
+ if (isNullish(data)) return null;
48
+ if (data === undefined) return data;
49
+
50
+ const n = +data;
51
+
52
+ if (isNaN(n)) {
53
+ throw new CastError("Expression value is not a number.");
54
+ }
55
+
56
+ return n;
57
+ }
58
+
59
+ protected BigInt(data: any) {
60
+ if (isNullish(data)) return null;
61
+
62
+ return BigInt(data);
63
+ }
64
+ }
@@ -0,0 +1,23 @@
1
+ import {SymbolMapper} from "./SymbolMapper.js";
2
+
3
+ describe("SymbolMapper", () => {
4
+ describe("deserialize()", () => {
5
+ it("should return value", () => {
6
+ const mapper = new SymbolMapper();
7
+
8
+ const value = mapper.deserialize("SYMBOL");
9
+
10
+ expect(typeof value).toEqual("symbol");
11
+ expect(value.toString()).toEqual("Symbol(SYMBOL)");
12
+ });
13
+ });
14
+ describe("serialize()", () => {
15
+ it("should return value", () => {
16
+ const mapper = new SymbolMapper();
17
+
18
+ const value = mapper.serialize(Symbol.for("SYMBOL"));
19
+
20
+ expect(value).toEqual("SYMBOL");
21
+ });
22
+ });
23
+ });
@@ -0,0 +1,19 @@
1
+ import {JsonMapper} from "../decorators/jsonMapper.js";
2
+ import {JsonMapperMethods} from "../interfaces/JsonMapperMethods.js";
3
+
4
+ /**
5
+ * Mapper for the `Symbol` type.
6
+ *
7
+ * @jsonmapper
8
+ * @component
9
+ */
10
+ @JsonMapper(Symbol)
11
+ export class SymbolMapper implements JsonMapperMethods {
12
+ deserialize(data: string): symbol {
13
+ return Symbol.for(data);
14
+ }
15
+
16
+ serialize(object: Symbol): any {
17
+ return object.toString().replace("Symbol(", "").replace(")", "");
18
+ }
19
+ }
@@ -0,0 +1,39 @@
1
+ import {JsonEntityFn, JsonHookContext} from "@tsed/schema";
2
+
3
+ export interface AfterDeserializeCallback {
4
+ (value: any, ctx: JsonHookContext): any;
5
+ }
6
+
7
+ /**
8
+ * Call the function after JsonMapper.
9
+ *
10
+ * ## Example
11
+ *
12
+ * ```typescript
13
+ * @AfterDeserialize((data: Model) => { // After deserialize get the instance class
14
+ * if (data.prop1 === 'VALUE' && data.prop2 !== 'VALUE2') {
15
+ * throw BadRequest('MyMessage'); // or throw a ValidationError
16
+ * } else {
17
+ * data.prop2 = 'VALUE2';
18
+ * return data;
19
+ * }
20
+ * })
21
+ * class Model {
22
+ * @Property()
23
+ * prop1: string;
24
+ * @Property()
25
+ * prop2: string;
26
+ * }
27
+ * ```
28
+ *
29
+ * @returns {Function}
30
+ * @decorator
31
+ * @schema
32
+ * @property
33
+ * @param cb
34
+ */
35
+ export function AfterDeserialize(cb: AfterDeserializeCallback): ClassDecorator {
36
+ return JsonEntityFn((storedJson) => {
37
+ storedJson.schema.$hooks.on("afterDeserialize", cb);
38
+ });
39
+ }
@@ -0,0 +1,39 @@
1
+ import {JsonEntityFn, JsonHookContext} from "@tsed/schema";
2
+
3
+ export interface BeforeDeserializeCallback {
4
+ (value: any, ctx: JsonHookContext): any;
5
+ }
6
+
7
+ /**
8
+ * Call the function before JsonMapper.
9
+ *
10
+ * ## Example
11
+ *
12
+ * ```typescript
13
+ * @BeforeDeserialize((data: Record<string, unknown>) => { // Before deserialize get the data
14
+ * if (data.prop1 === 'VALUE' && data.prop2 !== 'VALUE2') {
15
+ * throw BadRequest('MyMessage'); // or throw a ValidationError
16
+ * } else {
17
+ * data.prop2 = 'VALUE2';
18
+ * return data;
19
+ * }
20
+ * })
21
+ * class Model {
22
+ * @Property()
23
+ * prop1: string;
24
+ * @Property()
25
+ * prop2: string;
26
+ * }
27
+ * ```
28
+ *
29
+ * @returns {Function}
30
+ * @decorator
31
+ * @schema
32
+ * @property
33
+ * @param cb
34
+ */
35
+ export function BeforeDeserialize(cb: BeforeDeserializeCallback): ClassDecorator {
36
+ return JsonEntityFn((storedJson) => {
37
+ storedJson.schema.$hooks.on("beforeDeserialize", cb);
38
+ });
39
+ }
@@ -0,0 +1,16 @@
1
+ import {Type} from "@tsed/core";
2
+
3
+ import {registerJsonTypeMapper} from "../domain/JsonMapperTypesContainer.js";
4
+
5
+ /**
6
+ * Register a new JsonMapper Type
7
+ * @param types
8
+ * @constructor
9
+ */
10
+ export function JsonMapper(...types: (any | Type)[]): ClassDecorator {
11
+ return (target) => {
12
+ types.forEach((type) => {
13
+ registerJsonTypeMapper(type, target as any);
14
+ });
15
+ };
16
+ }
@@ -0,0 +1,29 @@
1
+ import {JsonEntityFn, JsonHookContext} from "@tsed/schema";
2
+
3
+ export interface OnDeserializeCallback {
4
+ (value: any, ctx: JsonHookContext): any;
5
+ }
6
+
7
+ /**
8
+ * Call the function before property deserialization.
9
+ *
10
+ * ## Example
11
+ *
12
+ * ```typescript
13
+ * class Model {
14
+ * @OnDeserialize(v => v + 1)
15
+ * property: string;
16
+ * }
17
+ * ```
18
+ *
19
+ * @returns {Function}
20
+ * @decorator
21
+ * @schema
22
+ * @property
23
+ * @param cb
24
+ */
25
+ export function OnDeserialize(cb: OnDeserializeCallback): PropertyDecorator {
26
+ return JsonEntityFn((storedJson) => {
27
+ storedJson.schema.$hooks.on("onDeserialize", cb);
28
+ });
29
+ }
@@ -0,0 +1,29 @@
1
+ import {JsonEntityFn, JsonHookContext} from "@tsed/schema";
2
+
3
+ export interface OnSerializeCallback {
4
+ (value: any, ctx: JsonHookContext): any;
5
+ }
6
+
7
+ /**
8
+ * Call the function before property serialization.
9
+ *
10
+ * ## Example
11
+ *
12
+ * ```typescript
13
+ * class Model {
14
+ * @OnSerialize(v => v + 1)
15
+ * property: string;
16
+ * }
17
+ * ```
18
+ *
19
+ * @returns {Function}
20
+ * @decorator
21
+ * @schema
22
+ * @property
23
+ * @param cb
24
+ */
25
+ export function OnSerialize(cb: OnSerializeCallback): PropertyDecorator {
26
+ return JsonEntityFn((storedJson) => {
27
+ storedJson.schema.$hooks.on("onSerialize", cb);
28
+ });
29
+ }