@tsed/json-mapper 8.44.2 → 8.44.3

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.
@@ -1,12 +1,19 @@
1
1
  import { JsonMapperCompiler } from "./JsonMapperCompiler.js";
2
2
  import { JsonParameterStore, JsonSchema, VendorKeys, getPropertiesStores, s } from "@tsed/schema";
3
- import { classOf, isArray, isBoolean, isEmpty, isNil, nameOf, objectKeys } from "@tsed/core";
3
+ import { classOf, isArray, isBoolean, isEmpty, isFunction, isNil, nameOf, objectKeys } from "@tsed/core";
4
4
  import { JsonMapperSettings } from "./JsonMapperSettings.js";
5
5
  import { Writer } from "./Writer.js";
6
6
  import { alterAfterDeserialize } from "../hooks/alterAfterDeserialize.js";
7
7
  import { alterBeforeDeserialize } from "../hooks/alterBeforeDeserialize.js";
8
8
  import { alterOnDeserialize } from "../hooks/alterOnDeserialize.js";
9
9
  import { getJsonMapperTypes } from "./JsonMapperTypesContainer.js";
10
+ /**
11
+ * Whether the mapper has a callable method named after the type. A property set to another value (e.g. `undefined`
12
+ * to disable the conversion) falls back to `deserialize()` instead of throwing.
13
+ */
14
+ function hasConversionMethod(mapper, name) {
15
+ return isFunction(mapper[name]);
16
+ }
10
17
  function isDeserializable(obj, options) {
11
18
  if ((!!options.collectionType && isNil(obj)) || obj === undefined) {
12
19
  return false;
@@ -38,6 +45,11 @@ export class JsonDeserializer extends JsonMapperCompiler {
38
45
  * @protected
39
46
  */
40
47
  this.constructors = {};
48
+ /**
49
+ * The type mappers registered from the types container, with the function each one was registered with
50
+ * @protected
51
+ */
52
+ this.typeMappers = new Map();
41
53
  this.addGlobal("newInstanceOf", this.newInstanceOf.bind(this));
42
54
  this.addGlobal("alterBeforeDeserialize", this.alterBeforeDeserialize.bind(this));
43
55
  this.addGlobal("alterAfterDeserialize", this.alterAfterDeserialize.bind(this));
@@ -198,6 +210,32 @@ export class JsonDeserializer extends JsonMapperCompiler {
198
210
  }
199
211
  return (writer) => writer.callMapper(nestedMapper.id, varKey(key), formatOpts);
200
212
  }
213
+ /**
214
+ * A type mapper must be registered again when it was never registered, when the mapper changed, when the mapper
215
+ * gained or lost the method named after the type, or when its entry was replaced or removed from the cache.
216
+ * The registered function reads the mapper methods at call time, so a replaced method needs no registration.
217
+ */
218
+ isTypeMapperOutdated(model, mapper) {
219
+ const registered = this.typeMappers.get(model);
220
+ if (!registered) {
221
+ return true;
222
+ }
223
+ const mapperChanged = registered.mapper !== mapper;
224
+ const hasConversion = hasConversionMethod(mapper, registered.name);
225
+ const conversionChanged = registered.hasConversion !== hasConversion;
226
+ // two models with the same name (Date and "Date") share the mapper table entry: the last registered one wins
227
+ const entryReplaced = this.cache.get(model)?.get("typeMapper")?.fn !== registered.fn || this.mappers[registered.name] !== registered.fn;
228
+ return mapperChanged || conversionChanged || entryReplaced;
229
+ }
230
+ registerTypeMapper(model, mapper) {
231
+ const name = nameOf(model);
232
+ const hasConversion = hasConversionMethod(mapper, name);
233
+ const fn = hasConversion
234
+ ? (value, options) => mapper[name](value, options)
235
+ : (value, options) => mapper.deserialize(value, { ...options, type: model });
236
+ this.addTypeMapper(model, fn);
237
+ this.typeMappers.set(model, { mapper, name, hasConversion, fn });
238
+ }
201
239
  mapOptions({ groups, useAlias = true, types, ...options }) {
202
240
  if (options.store instanceof JsonParameterStore) {
203
241
  return this.mapOptions(mapParamStoreOptions(options.store, options));
@@ -205,17 +243,8 @@ export class JsonDeserializer extends JsonMapperCompiler {
205
243
  const customMappers = {};
206
244
  types = types || getJsonMapperTypes();
207
245
  types.forEach((mapper, model) => {
208
- if (![Array, Set, Map].includes(model)) {
209
- const typeName = nameOf(model);
210
- if (nameOf(model) in mapper) {
211
- this.addTypeMapper(model, mapper[typeName].bind(mapper));
212
- }
213
- else {
214
- this.addTypeMapper(model, (value, options) => mapper.deserialize(value, {
215
- ...options,
216
- type: model
217
- }));
218
- }
246
+ if (model !== Array && model !== Set && model !== Map && this.isTypeMapperOutdated(model, mapper)) {
247
+ this.registerTypeMapper(model, mapper);
219
248
  }
220
249
  });
221
250
  const strictGroups = options.strictGroups ?? JsonMapperSettings.strictGroups;
@@ -254,37 +283,45 @@ export class JsonDeserializer extends JsonMapperCompiler {
254
283
  mapObject(input, options) {
255
284
  return input;
256
285
  }
257
- mapSet(input, options) {
286
+ // The collection mappers strip `id` from the options once, not once per item:
287
+ // `id` describes the items, it must not leak into the options of a nested mapper.
288
+ mapSet(input, { id, ...options }) {
258
289
  if (isNil(input)) {
259
290
  return input;
260
291
  }
261
292
  const obj = new Set();
262
- objectKeys(input).forEach((key) => {
263
- obj.add(this.mapItem(input[key], options));
264
- });
293
+ for (const key of objectKeys(input)) {
294
+ obj.add(this.execMapper(id, input[key], options));
295
+ }
265
296
  return obj;
266
297
  }
267
- mapArray(input, options) {
298
+ mapArray(input, { id, ...options }) {
268
299
  if (isNil(input)) {
269
300
  return input;
270
301
  }
271
- return [].concat(input).map((item) => {
272
- return this.mapItem(item, options);
273
- });
302
+ // a single value is mapped as a one-item array
303
+ const list = isArray(input) ? input : [input];
304
+ const out = new Array(list.length);
305
+ for (let i = 0; i < list.length; i++) {
306
+ const item = list[i];
307
+ // keep the holes of a sparse array: the item mapper is not called for them
308
+ if (item === undefined && !(i in list)) {
309
+ continue;
310
+ }
311
+ out[i] = this.execMapper(id, item, options);
312
+ }
313
+ return out;
274
314
  }
275
- mapMap(input, options) {
315
+ mapMap(input, { id, ...options }) {
276
316
  if (isNil(input)) {
277
317
  return input;
278
318
  }
279
319
  const obj = new Map();
280
- objectKeys(input).forEach((key) => {
281
- obj.set(key, this.mapItem(input[key], options));
282
- });
320
+ for (const key of objectKeys(input)) {
321
+ obj.set(key, this.execMapper(id, input[key], options));
322
+ }
283
323
  return obj;
284
324
  }
285
- mapItem(input, { id, ...options }) {
286
- return this.execMapper(id, input, options);
287
- }
288
325
  alterBeforeDeserialize(schemaId, value, options) {
289
326
  return alterBeforeDeserialize(value, this.schemes[schemaId], options);
290
327
  }
@@ -128,7 +128,8 @@ export class JsonSerializer extends JsonMapperCompiler {
128
128
  }
129
129
  const mapperChanged = registered.mapper !== mapper;
130
130
  const methodsChanged = registered.serialize !== mapper.serialize || registered.conversion !== mapper[registered.name];
131
- const entryReplaced = this.cache.get(model)?.get("typeMapper")?.fn !== registered.fn;
131
+ // two models with the same name (Date and "Date") share the mapper table entry: the last registered one wins
132
+ const entryReplaced = this.cache.get(model)?.get("typeMapper")?.fn !== registered.fn || this.mappers[registered.name] !== registered.fn;
132
133
  return mapperChanged || methodsChanged || entryReplaced;
133
134
  }
134
135
  mapOptions({ groups, useAlias = true, types, ...options }) {
@@ -1,6 +1,17 @@
1
- import { CachedGroupsJsonMapper, CachedJsonMapper, JsonMapperCompiler } from "./JsonMapperCompiler.js";
1
+ import { CachedGroupsJsonMapper, CachedJsonMapper, JsonMapperCallback, JsonMapperCompiler } from "./JsonMapperCompiler.js";
2
2
  import { Type } from "@tsed/core";
3
+ import { JsonMapperMethods } from "../interfaces/JsonMapperMethods.js";
3
4
  import { JsonDeserializerOptions } from "./JsonDeserializerOptions.js";
5
+ /**
6
+ * A registered type mapper. `hasConversion` tells whether the mapper had a method named after the type
7
+ * (e.g. `PrimitiveMapper.String`) when it was registered: the function calls that method, otherwise `deserialize()`.
8
+ */
9
+ type TypeMapperEntry = {
10
+ mapper: JsonMapperMethods;
11
+ name: string;
12
+ hasConversion: boolean;
13
+ fn: JsonMapperCallback<any>;
14
+ };
4
15
  /**
5
16
  * Deserializes raw JSON data into class instances based on schema metadata,
6
17
  * handling collections, discriminators, hooks, and registered type mappers.
@@ -11,6 +22,11 @@ export declare class JsonDeserializer extends JsonMapperCompiler<JsonDeserialize
11
22
  * @protected
12
23
  */
13
24
  protected constructors: Record<string, Type<any>>;
25
+ /**
26
+ * The type mappers registered from the types container, with the function each one was registered with
27
+ * @protected
28
+ */
29
+ protected typeMappers: Map<any, TypeMapperEntry>;
14
30
  constructor();
15
31
  map<Model = any>(input: any, options?: JsonDeserializerOptions): Model;
16
32
  compile(model: Type<any>, groups: false | string[]): CachedJsonMapper<JsonDeserializerOptions>;
@@ -26,14 +42,21 @@ export declare class JsonDeserializer extends JsonMapperCompiler<JsonDeserialize
26
42
  private mapDiscriminator;
27
43
  private mapProperty;
28
44
  private getPropertyFiller;
45
+ /**
46
+ * A type mapper must be registered again when it was never registered, when the mapper changed, when the mapper
47
+ * gained or lost the method named after the type, or when its entry was replaced or removed from the cache.
48
+ * The registered function reads the mapper methods at call time, so a replaced method needs no registration.
49
+ */
50
+ private isTypeMapperOutdated;
51
+ private registerTypeMapper;
29
52
  private mapOptions;
30
53
  private mapAdditionalProperties;
31
54
  private mapObject;
32
55
  private mapSet;
33
56
  private mapArray;
34
57
  private mapMap;
35
- private mapItem;
36
58
  private alterBeforeDeserialize;
37
59
  private alterAfterDeserialize;
38
60
  private mapGenericsOptions;
39
61
  }
62
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tsed/json-mapper",
3
- "version": "8.44.2",
3
+ "version": "8.44.3",
4
4
  "description": "Json mapper module for Ts.ED Framework",
5
5
  "keywords": [
6
6
  "TypeScript",
@@ -41,10 +41,10 @@
41
41
  "tslib": ">=2.7.0"
42
42
  },
43
43
  "devDependencies": {
44
- "@tsed/barrels": "8.44.2",
45
- "@tsed/core": "8.44.2",
46
- "@tsed/schema": "8.44.2",
47
- "@tsed/typescript": "8.44.2",
44
+ "@tsed/barrels": "8.44.3",
45
+ "@tsed/core": "8.44.3",
46
+ "@tsed/schema": "8.44.3",
47
+ "@tsed/typescript": "8.44.3",
48
48
  "temporal-polyfill": "1.0.1",
49
49
  "typescript": "6.0.2",
50
50
  "vitest": "4.1.4"