From a5cab62593985ab6e554692f63537d0c10c8fe47 Mon Sep 17 00:00:00 2001 From: Shakil Siraj Date: Mon, 11 Aug 2025 17:51:21 +1000 Subject: [PATCH 1/2] Completed feature ignoreDeserializer --- README.md | 85 +- RELEASES.md | 16 +- dist/ObjectMapper.d.ts | 5 +- package-lock.json | 27 +- package.json | 2 +- src/main/DeserializationHelper.ts | 592 +++++------ src/test/index.spec.ts | 1615 +++++++++++++++-------------- 7 files changed, 1218 insertions(+), 1124 deletions(-) diff --git a/README.md b/README.md index daa9532..c27d7e2 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ ## Introduction -`json-object-mapper` is a `typescript` library designed to serialize and -de-serialize DTO objects from and to JSON objects. Using the library, you would -be able to load the JSON data from `Http`/`File`/`Stream` stright -into an object graph of your DTO classes as well as serialize a DTO object graph -so that it can be sent to an output stream. - -The idea behind this is that you do not need to add serialization and -de-serialization methods to each of your DTO classes - thus keeping them clean +`json-object-mapper` is a `typescript` library designed to serialize and +de-serialize DTO objects from and to JSON objects. Using the library, you would +be able to load the JSON data from `Http`/`File`/`Stream` stright +into an object graph of your DTO classes as well as serialize a DTO object graph +so that it can be sent to an output stream. + +The idea behind this is that you do not need to add serialization and +de-serialization methods to each of your DTO classes - thus keeping them clean and simple. ![Build Status](https://github.com/shakilsiraj/json-object-mapper/actions/workflows/test.yml/badge.svg?branch=release/1.7) @@ -70,13 +70,13 @@ let testInstance: SimpleRoster = ObjectMapper.deserialize(SimpleRoster, json); expect(testInstance.isAvailableToday()).toBeFalsy(); ``` -First lets talk about the property `systemDate` and the `@JsonProperty` property decorator assinged -to it. As the `Date` type is not a primitive data type, you will need to explicitly mention that +First lets talk about the property `systemDate` and the `@JsonProperty` property decorator assinged +to it. As the `Date` type is not a primitive data type, you will need to explicitly mention that to the processor that we need to create a new instance of `Date` object with the supplied value and assign it to the `systemDate` property. When the `deserialize` method runs, it will first create a new instance of 'SimpleRoster' class. -Then it will parse the keys of the instance and assign the correct values to +Then it will parse the keys of the instance and assign the correct values to each keys. While doing that, it will make sure that the right property types are maintained - meaning that `name` field will be assinged a `String` object with the value 'John Doe', `numberOfHours` will be assigned a `Number` object and the `systemDate` field will be assinged a `Date` object with the value @@ -104,15 +104,15 @@ let stringrified: String = ObjectMapper.serialize(instance); expect(stringrified).toBe('{"firstName":"John","middleName":"P","lastName":"Doe","AKA":["John","Doe","JohnDoe","JohnPDoe"]}'); ``` -From the example above, the `knownAs` property is serialized as `AKA` as defined in the `@JsonProperty` -decorator. The `password` property does not serialized as defined in the `@JsonIgnore` +From the example above, the `knownAs` property is serialized as `AKA` as defined in the `@JsonProperty` +decorator. The `password` property does not serialized as defined in the `@JsonIgnore` decorator. -The library uses non-recursive iterations to process data. That means you can -serialize and de-serialize large amount of data quickly and efficiently using any -modern browser as well as native applications (such as nodejs, electron, +The library uses non-recursive iterations to process data. That means you can +serialize and de-serialize large amount of data quickly and efficiently using any +modern browser as well as native applications (such as nodejs, electron, nativescript, etc.). Please have a look at the `test` folder to see few examples -of using large dataset as well as how to use the library in general. +of using large dataset as well as how to use the library in general. ## Inspiration @@ -126,9 +126,9 @@ The project is hosted on [github.com](https://github.com/shakilsiraj/json-object npm install json-object-mapper --save ``` ## Depedency -The library depends on [reflect-metadata](https://www.npmjs.com/package/reflect-metadata) library. +The library depends on [reflect-metadata](https://www.npmjs.com/package/reflect-metadata) library. Originally, I thought of not having to force a dependency on another library. -But when you look the geneated JS code for a `typescript` decorator, +But when you look the geneated JS code for a `typescript` decorator, it is always trying to check for the variable `Reflect`. So, until such time when `typescript` decorators can be de-coupled from `Reflect` library, I am sticking with it. You can either download `reflect-metadata` from there or from npmjs.com with the `npm` command: @@ -167,6 +167,37 @@ aexpect(serialized).toBe('{"id":"1","geo-location":"Canberra"}'); ``` +### Ignoring special deserializers all together +If you have an use case where you want to not use specified deserializer in the metadata using de-serialization, the you can turn if off by setting `ignoreNameMetadata` to true (default is false). Here is an example usage: + +```typescript +class StringToNumberDeserializer implements Deserializer { + deserialize(value: string) { + return Number.parseFloat(value); + } +} + +class Event { + @JsonProperty() + id: number = undefined; + @JsonProperty({ deserializer: StringToNumberDeserializer }) + location = undefined; +} + +const json = { + id: "1", + location: "125.55" +}; + +const testInstance: Event = ObjectMapper.deserialize(Event, json, { + ignoreDeserializer: true +}); + +const serialized = ObjectMapper.serialize(testInstance); +expect(serialized).toBe('{"id":"1","location":"125.55"}'); + +``` + ### Enum serialization and de-serialization You can use `enum` data type by specifying the `type` property of @JsonProperty decorator. You will need to use `Serializer` and `Deserializer` to make the enum work correctly. @@ -186,12 +217,12 @@ class DaysEnumSerializerDeserializer implements Deserializer, Serializer{ enum Days{ Sun, Mon, Tues, Wed, Thurs, Fri, Sat -} +} class Workday{ @JsonProperty({ type: Days, deserializer: DaysEnumSerializerDeserializer, serializer: DaysEnumSerializerDeserializer}) today: Days; -} +} let json = { "today": 'Tues' }; @@ -222,11 +253,11 @@ class MapDeserializer implements Deserializer { ``` ### A special thing about Date object -It's very hard to get a `Date` instance right across all browsers - +It's very hard to get a `Date` instance right across all browsers - have a look at the [stackoverflow discussion](http://stackoverflow.com/questions/2587345/why-does-date-parse-give-incorrect-results). -The best way to manage this complexitiy I have found so far is to use the -`new Date(value)` constructor which takes the number of milliseconds since -1st January 1970 UTC. So, to best use this library, make sure that the date is +The best way to manage this complexitiy I have found so far is to use the +`new Date(value)` constructor which takes the number of milliseconds since +1st January 1970 UTC. So, to best use this library, make sure that the date is passed on as the number of milliseconds during deserializition: ```json jsonTest["dateType"] : 1333065600000; @@ -239,7 +270,7 @@ For serialization, it will only print out the milliseconds: ```json {"test":1457096400000} ``` -Also, you will need to use the `DateSerializer` or your own implementation for serializing `Date` objects. +Also, you will need to use the `DateSerializer` or your own implementation for serializing `Date` objects. ```typescript @JsonProperty({type: Date, name:'dateOfBirth', serializer: DateSerializer}) dob: Date = new Date(1483142400000) @@ -250,4 +281,4 @@ A special thanks to all who have contributed to this project. - \ No newline at end of file + diff --git a/RELEASES.md b/RELEASES.md index 3de89a1..28859d5 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,6 +1,14 @@ +### Release : 1.9.0 +Feature added: +* Feature - ignore deserializer [pr/84](https://github.com/shakilsiraj/json-object-mapper/pull/84) + +### Release : 1.8.1 +Feature added: +* Feature - ignore name metadata [pr/83](https://github.com/shakilsiraj/json-object-mapper/pull/83) + ### Release : 1.7.1 Fixes: -* Fix issue with npm publish github action +* Fix issue with npm publish github action ### Release : 1.7 Features added: @@ -16,7 +24,7 @@ Others: * Updated README.md ### Release : 1.6 -Features added: +Features added: * Jest integration * Upgraded libraries * Upgraded typescript version @@ -34,6 +42,6 @@ Features added: Defect fixes: * [Enable String Serializer to support strings with special characters](https://github.com/shakilsiraj/json-object-mapper/pull/6). Thanks [@ironchimp](https://github.com/ironchimp) -* [deserializeArray returns undefined when called on an empty array](https://github.com/shakilsiraj/json-object-mapper/pull/19). Thanks [@devpreview](https://github.com/devpreview) -* [nested array is undefined](https://github.com/shakilsiraj/json-object-mapper/pull/21). Thanks [@devpreview](https://github.com/devpreview) +* [deserializeArray returns undefined when called on an empty array](https://github.com/shakilsiraj/json-object-mapper/pull/19). Thanks [@devpreview](https://github.com/devpreview) +* [nested array is undefined](https://github.com/shakilsiraj/json-object-mapper/pull/21). Thanks [@devpreview](https://github.com/devpreview) * Finally fixed travis build with linting fix. diff --git a/dist/ObjectMapper.d.ts b/dist/ObjectMapper.d.ts index 10d7fd6..4b9fbe3 100644 --- a/dist/ObjectMapper.d.ts +++ b/dist/ObjectMapper.d.ts @@ -13,7 +13,7 @@ export interface JsonPropertyDecoratorMetadata { export enum AccessType { READ_ONLY, WRITE_ONLY, - BOTH, + BOTH } export interface Serializer { @@ -52,7 +52,8 @@ export declare function JsonIgnore(): any; export declare function JsonConverstionError(message: string, json: any): Error; export interface DeserializationConfig { - ignoreNameMetadata: boolean; + ignoreNameMetadata?: boolean; + ignoreDeserializer?: boolean; } export declare namespace ObjectMapper { diff --git a/package-lock.json b/package-lock.json index eadc349..f08f65f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "json-object-mapper", - "version": "1.7.2", + "version": "1.9.0-rc1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "json-object-mapper", - "version": "1.7.2", + "version": "1.9.0-rc1", "license": "MIT", "devDependencies": { "@types/es6-shim": "^0.31.42", @@ -3753,6 +3753,14 @@ "node": ">=8" } }, + "node_modules/jasmine-core": { + "version": "5.9.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.9.0.tgz", + "integrity": "sha512-OMUvF1iI6+gSRYOhMrH4QYothVLN9C3EJ6wm4g7zLJlnaTl8zbaPOr0bTw70l7QxkoM7sVFOWo83u9B2Fe2Zng==", + "dev": true, + "license": "MIT", + "peer": true + }, "node_modules/jest": { "version": "27.2.4", "resolved": "https://registry.npmjs.org/jest/-/jest-27.2.4.tgz", @@ -5602,6 +5610,21 @@ "node": ">=0.10.0" } }, + "node_modules/requirejs": { + "version": "2.3.7", + "resolved": "https://registry.npmjs.org/requirejs/-/requirejs-2.3.7.tgz", + "integrity": "sha512-DouTG8T1WanGok6Qjg2SXuCMzszOo0eHeH9hDZ5Y4x8Je+9JB38HdTLT4/VA8OaUhBa0JPVHJ0pyBkM1z+pDsw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "r_js": "bin/r.js", + "r.js": "bin/r.js" + }, + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", diff --git a/package.json b/package.json index c3d6e52..dc58b14 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-object-mapper", - "version": "1.8.1", + "version": "1.9.0-rc1", "description": "A TypeScript library to serialize and deserialize JSON objects in a fast and non-recursive way", "repository": { "type": "git", diff --git a/src/main/DeserializationHelper.ts b/src/main/DeserializationHelper.ts index 0e53060..9587d29 100644 --- a/src/main/DeserializationHelper.ts +++ b/src/main/DeserializationHelper.ts @@ -1,294 +1,298 @@ -import { - AccessType, - JsonConverstionError, - JsonPropertyDecoratorMetadata, -} from "./DecoratorMetadata"; -import { - Constants, - getCachedType, - getJsonPropertyDecoratorMetadata, - getTypeName, - getTypeNameFromInstance, - isArrayType, - isSimpleType, - METADATA_JSON_IGNORE_NAME, - METADATA_JSON_PROPERTIES_NAME, -} from "./ReflectHelper"; - -declare var Reflect; - -const SimpleTypeCoverter = (value: any, type: string): any => { - return type === Constants.DATE_TYPE ? new Date(value) : value; -}; - -/** - * Configuration to fine tune deserialization - */ -export interface DeserializationConfig { - ignoreNameMetadata: boolean; // ignore name metadata from JsonPropertyDecoratorMetadata -} - -/** - * Deserializes a standard js object type(string, number and boolean) from json. - */ -export const DeserializeSimpleType = ( - instance: Object, - instanceKey: string, - type: any, - json: any, - jsonKey: string -) => { - try { - instance[instanceKey] = json[jsonKey]; - return []; - } catch (e) { - // tslint:disable-next-line:no-string-literal - throw new JsonConverstionError( - `Property '${instanceKey}' of ${instance.constructor["name"]} does not match datatype of ${jsonKey}`, - json - ); - } -}; - -/** - * Deserializes a standard js Date object type from json. - */ -export const DeserializeDateType = ( - instance: Object, - instanceKey: string, - type: any, - json: any, - jsonKey: string -): Array => { - try { - instance[instanceKey] = new Date(json[jsonKey]); - return []; - } catch (e) { - // tslint:disable-next-line:no-string-literal - throw new JsonConverstionError( - `Property '${instanceKey}' of ${instance.constructor["name"]} does not match datatype of ${jsonKey}`, - json - ); - } -}; - -/** - * Deserializes a JS array type from json. - */ -export let DeserializeArrayType = ( - instance: Object, - instanceKey: string, - type: any, - json: Object, - jsonKey: string, - config: DeserializationConfig -): Array => { - const jsonObject = jsonKey !== undefined ? json[jsonKey] || [] : json; - const jsonArraySize = jsonObject.length; - const conversionFunctionsList = []; - const arrayInstance = []; - instance[instanceKey] = arrayInstance; - if (jsonArraySize > 0) { - for (let i = 0; i < jsonArraySize; i++) { - if (jsonObject[i]) { - const typeName = getTypeNameFromInstance(type); - if (!isSimpleType(typeName)) { - const typeInstance = new type(); - conversionFunctionsList.push({ - functionName: Constants.OBJECT_TYPE, - instance: typeInstance, - json: jsonObject[i], - undefined, - config, - }); - arrayInstance.push(typeInstance); - } else { - arrayInstance.push( - conversionFunctions[Constants.FROM_ARRAY](jsonObject[i], typeName) - ); - } - } - } - } - return conversionFunctionsList; -}; - -/** - * Deserializes a js object type from json. - */ -export const DeserializeComplexType = ( - instance: Object, - instanceKey: string, - type: any, - json: any, - jsonKey: string, - config: DeserializationConfig -): Array => { - const conversionFunctionsList = []; - - let objectInstance; - /** - * If instanceKey is not passed on then it's the first iteration of the functions. - */ - // tslint:disable-next-line:triple-equals - if (instanceKey != undefined) { - objectInstance = new type(); - instance[instanceKey] = objectInstance; - } else { - objectInstance = instance; - } - - let objectKeys: string[] = Object.keys(objectInstance); - objectKeys = objectKeys.concat( - ( - Reflect.getMetadata(METADATA_JSON_PROPERTIES_NAME, objectInstance) || [] - ).filter((item: string) => { - if ( - objectInstance.constructor.prototype.hasOwnProperty(item) && - Object.getOwnPropertyDescriptor( - objectInstance.constructor.prototype, - item - ).set === undefined - ) { - // Property does not have setter - return false; - } - return objectKeys.indexOf(item) < 0; - }) - ); - objectKeys = objectKeys.filter((item: string) => { - return !Reflect.hasMetadata( - METADATA_JSON_IGNORE_NAME, - objectInstance, - item - ); - }); - objectKeys.forEach((key: string) => { - /** - * Check if there is any DecoratorMetadata attached to this property, otherwise create a new one. - */ - let metadata: JsonPropertyDecoratorMetadata = getJsonPropertyDecoratorMetadata( - objectInstance, - key - ); - if (metadata === undefined) { - metadata = { name: key, required: false, access: AccessType.BOTH }; - } - // tslint:disable-next-line:triple-equals - if (AccessType.WRITE_ONLY != metadata.access) { - /** - * Check requried property - */ - if (metadata.required && json[metadata.name] === undefined) { - throw new JsonConverstionError( - `JSON structure does have have required property '${ - metadata.name - }' as required by '${getTypeNameFromInstance( - objectInstance - )}[${key}]`, - json - ); - } - // tslint:disable-next-line:triple-equals - const jsonKeyName = - config.ignoreNameMetadata === true ? key : metadata.name || key; - // tslint:disable-next-line:triple-equals - if (json[jsonKeyName] != undefined) { - /** - * If metadata has deserializer, use that one instead. - */ - // tslint:disable-next-line:triple-equals - if (metadata.deserializer != undefined) { - objectInstance[key] = getOrCreateDeserializer( - metadata.deserializer - ).deserialize(json[jsonKeyName]); - } else if (metadata.type === undefined) { - /** - * If we do not have any type defined, then we can't do much here but to hope for the best. - */ - objectInstance[key] = json[jsonKeyName]; - } else { - if (!isArrayType(objectInstance, key)) { - // tslint:disable-next-line:triple-equals - const typeName = - metadata.type != undefined - ? getTypeNameFromInstance(metadata.type) - : getTypeName(objectInstance, key); - if (!isSimpleType(typeName)) { - objectInstance[key] = new metadata.type(); - conversionFunctionsList.push({ - functionName: Constants.OBJECT_TYPE, - type: metadata.type, - instance: objectInstance[key], - json: json[jsonKeyName], - jsonKeyName, - config, - }); - } else { - conversionFunctions[typeName]( - objectInstance, - key, - typeName, - json, - jsonKeyName - ); - } - } else { - const moreFunctions: Array = conversionFunctions[ - Constants.ARRAY_TYPE - ](objectInstance, key, metadata.type, json, jsonKeyName); - moreFunctions.forEach((struct: ConversionFunctionStructure) => { - conversionFunctionsList.push(struct); - }); - } - } - } - } - }); - - return conversionFunctionsList; -}; - -/** - * Conversion function parameters structure that will be used to call the function. - */ -export interface ConversionFunctionStructure { - functionName: string; - instance: any; - instanceKey?: string; - type?: any; - json: any; - jsonKey?: string; -} - -/** - * Object to cache deserializers - */ -export const deserializers = {}; - -/** - * Checks to see if the deserializer already exists or not. - * If not, creates a new one and caches it, returns the - * cached instance otherwise. - */ -export const getOrCreateDeserializer = (type: any): any => { - return getCachedType(type, deserializers); -}; - -/** - * List of JSON object conversion functions. - */ -export const conversionFunctions = {}; -conversionFunctions[Constants.OBJECT_TYPE] = DeserializeComplexType; -conversionFunctions[Constants.ARRAY_TYPE] = DeserializeArrayType; -conversionFunctions[Constants.DATE_TYPE] = DeserializeDateType; -conversionFunctions[Constants.STRING_TYPE] = DeserializeSimpleType; -conversionFunctions[Constants.NUMBER_TYPE] = DeserializeSimpleType; -conversionFunctions[Constants.BOOLEAN_TYPE] = DeserializeSimpleType; -conversionFunctions[Constants.FROM_ARRAY] = SimpleTypeCoverter; -conversionFunctions[Constants.OBJECT_TYPE_LOWERCASE] = DeserializeComplexType; -conversionFunctions[Constants.ARRAY_TYPE_LOWERCASE] = DeserializeArrayType; -conversionFunctions[Constants.DATE_TYPE_LOWERCASE] = DeserializeDateType; -conversionFunctions[Constants.STRING_TYPE_LOWERCASE] = DeserializeSimpleType; -conversionFunctions[Constants.NUMBER_TYPE_LOWERCASE] = DeserializeSimpleType; -conversionFunctions[Constants.BOOLEAN_TYPE_LOWERCASE] = DeserializeSimpleType; +import { + AccessType, + JsonConverstionError, + JsonPropertyDecoratorMetadata +} from "./DecoratorMetadata"; +import { + Constants, + getCachedType, + getJsonPropertyDecoratorMetadata, + getTypeName, + getTypeNameFromInstance, + isArrayType, + isSimpleType, + METADATA_JSON_IGNORE_NAME, + METADATA_JSON_PROPERTIES_NAME +} from "./ReflectHelper"; + +declare var Reflect; + +const SimpleTypeCoverter = (value: any, type: string): any => { + return type === Constants.DATE_TYPE ? new Date(value) : value; +}; + +/** + * Configuration to fine tune deserialization + */ +export interface DeserializationConfig { + ignoreNameMetadata?: boolean; // ignore name metadata from JsonPropertyDecoratorMetadata + ignoreDeserializer?: boolean; // ignore name metadata from JsonPropertyDecoratorMetadata +} + +/** + * Deserializes a standard js object type(string, number and boolean) from json. + */ +export const DeserializeSimpleType = ( + instance: Object, + instanceKey: string, + type: any, + json: any, + jsonKey: string +) => { + try { + instance[instanceKey] = json[jsonKey]; + return []; + } catch (e) { + // tslint:disable-next-line:no-string-literal + throw new JsonConverstionError( + `Property '${instanceKey}' of ${instance.constructor["name"]} does not match datatype of ${jsonKey}`, + json + ); + } +}; + +/** + * Deserializes a standard js Date object type from json. + */ +export const DeserializeDateType = ( + instance: Object, + instanceKey: string, + type: any, + json: any, + jsonKey: string +): Array => { + try { + instance[instanceKey] = new Date(json[jsonKey]); + return []; + } catch (e) { + // tslint:disable-next-line:no-string-literal + throw new JsonConverstionError( + `Property '${instanceKey}' of ${instance.constructor["name"]} does not match datatype of ${jsonKey}`, + json + ); + } +}; + +/** + * Deserializes a JS array type from json. + */ +export let DeserializeArrayType = ( + instance: Object, + instanceKey: string, + type: any, + json: Object, + jsonKey: string, + config: DeserializationConfig +): Array => { + const jsonObject = jsonKey !== undefined ? json[jsonKey] || [] : json; + const jsonArraySize = jsonObject.length; + const conversionFunctionsList = []; + const arrayInstance = []; + instance[instanceKey] = arrayInstance; + if (jsonArraySize > 0) { + for (let i = 0; i < jsonArraySize; i++) { + if (jsonObject[i]) { + const typeName = getTypeNameFromInstance(type); + if (!isSimpleType(typeName)) { + const typeInstance = new type(); + conversionFunctionsList.push({ + functionName: Constants.OBJECT_TYPE, + instance: typeInstance, + json: jsonObject[i], + undefined, + config + }); + arrayInstance.push(typeInstance); + } else { + arrayInstance.push( + conversionFunctions[Constants.FROM_ARRAY](jsonObject[i], typeName) + ); + } + } + } + } + return conversionFunctionsList; +}; + +/** + * Deserializes a js object type from json. + */ +export const DeserializeComplexType = ( + instance: Object, + instanceKey: string, + type: any, + json: any, + jsonKey: string, + config: DeserializationConfig +): Array => { + const conversionFunctionsList = []; + + let objectInstance; + /** + * If instanceKey is not passed on then it's the first iteration of the functions. + */ + // tslint:disable-next-line:triple-equals + if (instanceKey != undefined) { + objectInstance = new type(); + instance[instanceKey] = objectInstance; + } else { + objectInstance = instance; + } + + let objectKeys: string[] = Object.keys(objectInstance); + objectKeys = objectKeys.concat( + ( + Reflect.getMetadata(METADATA_JSON_PROPERTIES_NAME, objectInstance) || [] + ).filter((item: string) => { + if ( + objectInstance.constructor.prototype.hasOwnProperty(item) && + Object.getOwnPropertyDescriptor( + objectInstance.constructor.prototype, + item + ).set === undefined + ) { + // Property does not have setter + return false; + } + return objectKeys.indexOf(item) < 0; + }) + ); + objectKeys = objectKeys.filter((item: string) => { + return !Reflect.hasMetadata( + METADATA_JSON_IGNORE_NAME, + objectInstance, + item + ); + }); + objectKeys.forEach((key: string) => { + /** + * Check if there is any DecoratorMetadata attached to this property, otherwise create a new one. + */ + let metadata: JsonPropertyDecoratorMetadata = getJsonPropertyDecoratorMetadata( + objectInstance, + key + ); + if (metadata === undefined) { + metadata = { name: key, required: false, access: AccessType.BOTH }; + } + // tslint:disable-next-line:triple-equals + if (AccessType.WRITE_ONLY != metadata.access) { + /** + * Check requried property + */ + if (metadata.required && json[metadata.name] === undefined) { + throw new JsonConverstionError( + `JSON structure does have have required property '${ + metadata.name + }' as required by '${getTypeNameFromInstance( + objectInstance + )}[${key}]`, + json + ); + } + // tslint:disable-next-line:triple-equals + const jsonKeyName = + config.ignoreNameMetadata === true ? key : metadata.name || key; + // tslint:disable-next-line:triple-equals + if (json[jsonKeyName] != undefined) { + /** + * If metadata has deserializer, use that one instead. + */ + // tslint:disable-next-line:triple-equals + if ( + metadata.deserializer != undefined && + !!!config.ignoreDeserializer + ) { + objectInstance[key] = getOrCreateDeserializer( + metadata.deserializer + ).deserialize(json[jsonKeyName]); + } else if (metadata.type === undefined) { + /** + * If we do not have any type defined, then we can't do much here but to hope for the best. + */ + objectInstance[key] = json[jsonKeyName]; + } else { + if (!isArrayType(objectInstance, key)) { + // tslint:disable-next-line:triple-equals + const typeName = + metadata.type != undefined + ? getTypeNameFromInstance(metadata.type) + : getTypeName(objectInstance, key); + if (!isSimpleType(typeName)) { + objectInstance[key] = new metadata.type(); + conversionFunctionsList.push({ + functionName: Constants.OBJECT_TYPE, + type: metadata.type, + instance: objectInstance[key], + json: json[jsonKeyName], + jsonKeyName, + config + }); + } else { + conversionFunctions[typeName]( + objectInstance, + key, + typeName, + json, + jsonKeyName + ); + } + } else { + const moreFunctions: Array = conversionFunctions[ + Constants.ARRAY_TYPE + ](objectInstance, key, metadata.type, json, jsonKeyName); + moreFunctions.forEach((struct: ConversionFunctionStructure) => { + conversionFunctionsList.push(struct); + }); + } + } + } + } + }); + + return conversionFunctionsList; +}; + +/** + * Conversion function parameters structure that will be used to call the function. + */ +export interface ConversionFunctionStructure { + functionName: string; + instance: any; + instanceKey?: string; + type?: any; + json: any; + jsonKey?: string; +} + +/** + * Object to cache deserializers + */ +export const deserializers = {}; + +/** + * Checks to see if the deserializer already exists or not. + * If not, creates a new one and caches it, returns the + * cached instance otherwise. + */ +export const getOrCreateDeserializer = (type: any): any => { + return getCachedType(type, deserializers); +}; + +/** + * List of JSON object conversion functions. + */ +export const conversionFunctions = {}; +conversionFunctions[Constants.OBJECT_TYPE] = DeserializeComplexType; +conversionFunctions[Constants.ARRAY_TYPE] = DeserializeArrayType; +conversionFunctions[Constants.DATE_TYPE] = DeserializeDateType; +conversionFunctions[Constants.STRING_TYPE] = DeserializeSimpleType; +conversionFunctions[Constants.NUMBER_TYPE] = DeserializeSimpleType; +conversionFunctions[Constants.BOOLEAN_TYPE] = DeserializeSimpleType; +conversionFunctions[Constants.FROM_ARRAY] = SimpleTypeCoverter; +conversionFunctions[Constants.OBJECT_TYPE_LOWERCASE] = DeserializeComplexType; +conversionFunctions[Constants.ARRAY_TYPE_LOWERCASE] = DeserializeArrayType; +conversionFunctions[Constants.DATE_TYPE_LOWERCASE] = DeserializeDateType; +conversionFunctions[Constants.STRING_TYPE_LOWERCASE] = DeserializeSimpleType; +conversionFunctions[Constants.NUMBER_TYPE_LOWERCASE] = DeserializeSimpleType; +conversionFunctions[Constants.BOOLEAN_TYPE_LOWERCASE] = DeserializeSimpleType; diff --git a/src/test/index.spec.ts b/src/test/index.spec.ts index 4616731..6cb2487 100644 --- a/src/test/index.spec.ts +++ b/src/test/index.spec.ts @@ -1,794 +1,821 @@ -import "reflect-metadata"; -import { - AccessType, - Deserializer, - JsonIgnore, - JsonProperty, - JsonPropertyDecoratorMetadata, - Serializer, -} from "../main/DecoratorMetadata"; -import { getOrCreateDeserializer } from "../main/DeserializationHelper"; -import { ObjectMapper } from "../main/index"; -import { - DateSerializer, - getOrCreateSerializer, -} from "../main/SerializationHelper"; -import { a, b } from "./NameSpaces"; - -describe("Testing deserialize functions", () => { - it("Testing Class type with no annotations and 0 children", () => { - class NameTypeWithoutAnnotations { - firstName: string = undefined; - lastName: string = undefined; - middleName: string = undefined; - - public getFirstName(): string { - return this.firstName; - } - } - const nameTypeWithoutAnnotationsJson = { - firstName: "John", - middleName: "P", - lastName: "Doe", - }; - - const processedDto: NameTypeWithoutAnnotations = ObjectMapper.deserialize( - NameTypeWithoutAnnotations, - nameTypeWithoutAnnotationsJson - ); - expect(processedDto.getFirstName()).toBe("John"); - expect(processedDto.middleName).toBe("P"); - expect(processedDto.lastName).toBe("Doe"); - }); - - class Event {} - - class EventsArray { - @JsonProperty({ type: Event }) - eventsArray: Event[] = undefined; - } -}); - -describe("Testing serialize array function", () => { - it("Testing array serialization", () => { - class Event { - id: number = undefined; - location: string = undefined; - constructor(id: number, location: string) { - this.id = id; - this.location = location; - } - } - const eventsArray: Event[] = [ - new Event(1, "Canberra"), - new Event(2, "Sydney"), - new Event(3, "Melbourne"), - ]; - - const serializedString: String = ObjectMapper.serialize(eventsArray); - expect(serializedString).toBe( - `[{"id":1,"location":"Canberra"},{"id":2,"location":"Sydney"},{"id":3,"location":"Melbourne"}]` - ); - }); -}); - -describe("Testing deserialize array function", () => { - it("Testing array serialization 1", () => { - class Event { - id: number = undefined; - location: string = undefined; - } - - const json = JSON.parse( - '[{"id":1,"location":"Canberra"},{"id":2,"location":"Sydney"},{"id":3,"location":"Melbourne"}]' - ); - - const eventsArray: Event[] = ObjectMapper.deserializeArray(Event, json); - expect(eventsArray.length > 0); - expect(eventsArray[0].id).toBe(1); - expect(eventsArray[0].location).toBe("Canberra"); - expect(eventsArray[1].id).toBe(2); - expect(eventsArray[1].location).toBe("Sydney"); - expect(eventsArray[2].id).toBe(3); - expect(eventsArray[2].location).toBe("Melbourne"); - }); - - it("Deserialize array with one null value", () => { - class Event { - id: number = undefined; - location: string = undefined; - } - - const json = JSON.parse( - '[{"id":1,"location":"Canberra"},{"id":2,"location":"Sydney"}, {"id":3,"location":"Melbourne"}, null]' - ); - const eventsArray: Event[] = ObjectMapper.deserializeArray(Event, json); - expect(eventsArray.length > 0); - expect(eventsArray[0].id).toBe(1); - expect(eventsArray[0].location).toBe("Canberra"); - expect(eventsArray[1].id).toBe(2); - expect(eventsArray[1].location).toBe("Sydney"); - expect(eventsArray[2].id).toBe(3); - expect(eventsArray[2].location).toBe("Melbourne"); - }); - - it("Testing array serialization 2", () => { - class Friend { - id: number = undefined; - name: string = undefined; - uuid: string = undefined; - age: number = undefined; - email: string = undefined; - gender: string = undefined; - } - - const json = [ - { - id: 0, - name: "Kim Hernandez", - uuid: "5a8f55ea-f667-489a-b29f-13e1e6594963", - age: 20, - email: "kimhernandez@comverges.com", - gender: "female", - }, - { - id: 1, - name: "Sophie Hudson", - uuid: "c61d5c41-e807-4ff1-ae88-19eeb5429411", - age: 33, - email: "sophiehudson@comverges.com", - gender: "female", - }, - { - id: 2, - name: "Rowland Gates", - uuid: "6ce0a1bd-c955-4fb7-a89a-f9cca038de5e", - age: 26, - email: "rowlandgates@comverges.com", - gender: "male", - }, - { - id: 3, - name: "Madeline Ewing", - uuid: "678fa258-2481-4457-965d-5d8571cb59cc", - age: 25, - email: "madelineewing@comverges.com", - gender: "female", - }, - { - id: 4, - name: "Stevens Ryan", - uuid: "8b67b198-7eb9-4bb6-a317-315a22c89c1d", - age: 40, - email: "stevensryan@comverges.com", - gender: "male", - }, - { - id: 5, - name: "Malone Chang", - uuid: "2859f03b-a648-478f-bfd9-f913993dfe74", - age: 34, - email: "malonechang@comverges.com", - gender: "male", - }, - { - id: 6, - name: "Arlene Small", - uuid: "bb3a9e09-4748-47a5-8a9c-ad1c51a43399", - age: 39, - email: "arlenesmall@comverges.com", - gender: "female", - }, - { - id: 7, - name: "Josefa Blackburn", - uuid: "f858dbd4-f4f3-4f0e-9601-854c31fb73bb", - age: 40, - email: "josefablackburn@comverges.com", - gender: "female", - }, - { - id: 8, - name: "Dorothea Lopez", - uuid: "ddce2735-8aa0-4aca-8b6a-42f42bedcc73", - age: 22, - email: "dorothealopez@comverges.com", - gender: "female", - }, - { - id: 9, - name: "Cecile Soto", - uuid: "1fab793a-a691-4185-ba36-8023d961cee7", - age: 40, - email: "cecilesoto@comverges.com", - gender: "female", - }, - { - id: 10, - name: "Barrett Pope", - uuid: "7a4ceca2-95a4-4894-987a-69c86b98a313", - age: 33, - email: "barrettpope@comverges.com", - gender: "male", - }, - { - id: 11, - name: "Eliza Torres", - uuid: "f24351ff-ee2f-4483-b525-fd7cef62d56c", - age: 39, - email: "elizatorres@comverges.com", - gender: "female", - }, - { - id: 12, - name: "Baxter Cannon", - uuid: "ba1d7536-637f-412b-a2ba-0eab28c1b156", - age: 29, - email: "baxtercannon@comverges.com", - gender: "male", - }, - { - id: 13, - name: "Greene Martin", - uuid: "80d4aa47-3e13-4f77-835d-97f030188488", - age: 33, - email: "greenemartin@comverges.com", - gender: "male", - }, - { - id: 14, - name: "Mckinney Lowe", - uuid: "6852df51-efb3-4fdc-baed-4cd90a97fb39", - age: 22, - email: "mckinneylowe@comverges.com", - gender: "male", - }, - { - id: 15, - name: "Chambers Sloan", - uuid: "f1e7a0c0-7620-40a6-84cd-b5578b20cad5", - age: 26, - email: "chamberssloan@comverges.com", - gender: "male", - }, - { - id: 16, - name: "Lynne Gillespie", - uuid: "1d6465a1-2881-4a58-96a8-2e5ccfddfbf4", - age: 33, - email: "lynnegillespie@comverges.com", - gender: "female", - }, - { - id: 17, - name: "Fern York", - uuid: "34e25ab4-e268-468f-bb16-e8a2390fbd72", - age: 34, - email: "fernyork@comverges.com", - gender: "female", - }, - { - id: 18, - name: "Diaz Shelton", - uuid: "896c2c51-a8af-4c42-b069-13d4437a97f3", - age: 30, - email: "diazshelton@comverges.com", - gender: "male", - }, - { - id: 19, - name: "Carol Lindsay", - uuid: "4809bba6-4986-4501-9bc5-0437f22d1e2e", - age: 26, - email: "carollindsay@comverges.com", - gender: "female", - }, - { - id: 20, - name: "Eugenia Day", - uuid: "36e116d2-2d99-4052-a957-9b9a7e75d84f", - age: 38, - email: "eugeniaday@comverges.com", - gender: "female", - }, - { - id: 21, - name: "Marsha Bradford", - uuid: "e57a3802-1173-452c-ace6-d60bf13b7b88", - age: 35, - email: "marshabradford@comverges.com", - gender: "female", - }, - { - id: 22, - name: "Avila Saunders", - uuid: "161919ec-65aa-4875-b7f6-68c684a2f57a", - age: 40, - email: "avilasaunders@comverges.com", - gender: "male", - }, - { - id: 23, - name: "Hartman Herman", - uuid: "b71a026e-7909-4105-af1a-3d314821edba", - age: 20, - email: "hartmanherman@comverges.com", - gender: "male", - }, - { - id: 24, - name: "Yolanda Rodriguez", - uuid: "21ec9746-7c04-42b9-a492-ced69e5203c9", - age: 29, - email: "yolandarodriguez@comverges.com", - gender: "female", - }, - { - id: 25, - name: "Head Nichols", - uuid: "a514f653-c0ac-4028-921e-43bd3c32c14c", - age: 20, - email: "headnichols@comverges.com", - gender: "male", - }, - { - id: 26, - name: "Albert Gardner", - uuid: "a962a277-81fc-425a-b320-e676d957ab06", - age: 24, - email: "albertgardner@comverges.com", - gender: "male", - }, - { - id: 27, - name: "Claudine Wells", - uuid: "4d9d781b-0215-47fa-8c24-720058c2bbec", - age: 21, - email: "claudinewells@comverges.com", - gender: "female", - }, - { - id: 28, - name: "Addie Long", - uuid: "16712b38-99e9-487b-be88-dd9f31206360", - age: 33, - email: "addielong@comverges.com", - gender: "female", - }, - { - id: 29, - name: "Deidre Puckett", - uuid: "f3cf9b5b-f7b6-46cc-8a75-57634c6bf3f8", - age: 34, - email: "deidrepuckett@comverges.com", - gender: "female", - }, - ]; - - const friends: Friend[] = ObjectMapper.deserializeArray(Friend, json); - expect(friends.length).toBe(30); - - expect(friends[15].name).toBe("Chambers Sloan"); - expect(friends[29].email).toBe("deidrepuckett@comverges.com"); - expect(friends[25].uuid).toBe("a514f653-c0ac-4028-921e-43bd3c32c14c"); - expect(friends[18].gender).toBe("male"); - }); -}); - -describe("Testing serialize functions", () => { - it("Testing Class type with no annotations and 0 children", () => { - const SimpleClassJson = { - firstName: "John", - middleName: "P", - lastName: "Doe", - }; - - const stringrified: String = ObjectMapper.serialize(SimpleClassJson); - // console.log(JSON.stringify(SimpleClassJson)); - expect(stringrified).toBe( - `{"firstName":"John","middleName":"P","lastName":"Doe"}` - ); - }); - - it("Testing Class type with no annotations and an array", () => { - class SimpleClass { - firstName = "John"; - middleName = "P"; - lastName = "Doe"; - @JsonProperty({ type: String, name: "AKA" }) - knownAs: String[] = ["John", "Doe", "JohnDoe", "JohnPDoe"]; - @JsonProperty({ - type: Date, - name: "dateOfBirth", - serializer: DateSerializer, - }) - dob: Date = new Date(1483142400000); // Sat Dec 31, 2016 - } - - const intance: SimpleClass = new SimpleClass(); - - const stringrified: String = ObjectMapper.serialize(intance); - expect(stringrified).toBe( - `{"firstName":"John","middleName":"P","lastName":"Doe","dateOfBirth":1483142400000,"AKA":["John","Doe","JohnDoe","JohnPDoe"]}` - ); - }); - - it("Test all simple type properties", () => { - class SimpleRoster { - private name: String = undefined; - private worksOnWeekend: Boolean = undefined; - private numberOfHours: Number = undefined; - @JsonProperty({ type: Date }) - private systemDate: Date = undefined; - - public isAvailableToday(): Boolean { - if ( - this.systemDate.getDay() % 6 === 0 && - this.worksOnWeekend === false - ) { - return false; - } - return true; - } - } - const json = { - name: "John Doe", - worksOnWeekend: false, - numberOfHours: 8, - systemDate: 1483142400000, // Sat Dec 31, 2016 - }; - - const testInstance: SimpleRoster = ObjectMapper.deserialize( - SimpleRoster, - json - ); - expect(testInstance.isAvailableToday()).toBeFalsy(); - }); -}); - -describe("Testing NameSpaces", () => { - it("Test 1", () => { - const random1 = Math.random(); - const random2 = Date.now(); - - const json = { - c: "This is a test", - d: { - f: random1, - t: random2, - }, - }; - - const testInstance = ObjectMapper.deserialize(b.NamespaceBClass, json); - expect(testInstance.d.f).toBe(random1); - expect(testInstance.d.t).toBe(random2); - }); -}); - -describe("Misc tests", () => { - it("Tesing Dto with functions", () => { - class Roster { - private name: string = undefined; - private worksOnWeekend = false; - private today: Date = new Date(); - public isAvailable(date: Date): boolean { - if (date.getDay() % 6 === 0 && this.worksOnWeekend === false) { - return false; - } - return true; - } - public isAvailableToday(): boolean { - return this.isAvailable(this.today); - } - } - - const json = { - name: "John Doe", - worksOnWeekend: false, - }; - - const testInstance: Roster = ObjectMapper.deserialize(Roster, json); - expect(testInstance.isAvailable(new Date("2016-12-17"))).toBeFalsy(); - expect(testInstance.isAvailable(new Date("2016-12-16"))).toBeTruthy(); - expect(testInstance.isAvailableToday()).toBe( - new Date().getDay() % 6 === 0 ? false : true - ); - }); - - it("Testing enum array serializing", () => { - enum Days { - Sun, - Mon, - Tues, - Wed, - Thurs, - Fri, - Sat, - } - - class DaysEnumArraySerializer implements Serializer { - serialize = (values: Days[]): any => { - let result: string[] = values.map((value: Days) => Days[value]); - return JSON.stringify(result); - }; - } - - class Calendar { - month: string = undefined; - @JsonProperty({ type: Days, serializer: DaysEnumArraySerializer }) - days: Days[] = undefined; - } - - let cal: Calendar = new Calendar(); - cal.month = "June"; - cal.days = [Days.Mon, Days.Tues]; - const serialized: String = ObjectMapper.serialize(cal); - expect(serialized).toBe(`{"month":"June","days":["Mon","Tues"]}`); - }); - - it("Testing enum ", () => { - class DaysEnumSerializerDeserializer implements Deserializer, Serializer { - deserialize = (value: string): Days => { - return Days[value]; - }; - serialize = (value: Days): string => { - return `"${Days[value]}"`; - }; - } - - enum Days { - Sun, - Mon, - Tues, - Wed, - Thurs, - Fri, - Sat, - } - - class Workday { - @JsonProperty({ - type: Days, - deserializer: DaysEnumSerializerDeserializer, - serializer: DaysEnumSerializerDeserializer, - }) - today: Days = undefined; - } - - const json = { today: "Tues" }; - - const testInstance: Workday = ObjectMapper.deserialize(Workday, json); - expect(testInstance.today === Days.Tues).toBeTruthy(); - testInstance.today = Days.Fri; - const serialized: String = ObjectMapper.serialize(testInstance); - expect(serialized).toBe(`{"today":"Fri"}`); - }); - - it("Testing AccessType.READ_ONLY", () => { - class SimpleClass { - firstName = "John"; - middleName = "P"; - lastName = "Doe"; - @JsonProperty({ type: String, name: "AKA", access: AccessType.READ_ONLY }) - knownAs: String[] = ["John", "Doe", "JohnDoe", "JohnPDoe"]; - } - - const intance: SimpleClass = new SimpleClass(); - - const stringrified: String = ObjectMapper.serialize(intance); - expect(stringrified).toBe( - `{"firstName":"John","middleName":"P","lastName":"Doe"}` - ); - }); - - it("Testing AccessType.WRITE_ONLY", () => { - class Roster { - @JsonProperty({ access: AccessType.WRITE_ONLY }) - private name: string = undefined; - private worksOnWeekend = false; - private today: Date = new Date(); - public isAvailable(date: Date): boolean { - if (date.getDay() % 6 === 0 && this.worksOnWeekend === false) { - return false; - } - return true; - } - public isAvailableToday(): boolean { - return this.isAvailable(this.today); - } - public getName(): String { - return this.name; - } - } - - const json = { - name: "John Doe", - worksOnWeekend: false, - }; - - const testInstance: Roster = ObjectMapper.deserialize(Roster, json); - expect(testInstance.getName()).toBeUndefined(); - }); - - it("Testing deserializer and serializer instances", () => { - class TestSerailizer implements Serializer { - serialize = (value: any): any => {}; - } - - const instance1: TestSerailizer = getOrCreateSerializer(TestSerailizer); - const instance2: TestSerailizer = getOrCreateSerializer(TestSerailizer); - expect(instance1).toBe(instance2); - - class TestDeserializer implements Deserializer { - deserialize = (value: any): any => {}; - } - - const instance3: TestDeserializer = getOrCreateDeserializer( - TestDeserializer - ); - const instance4: TestDeserializer = getOrCreateDeserializer( - TestDeserializer - ); - expect(instance3).toBe(instance4); - }); -}); - -describe("Testing JsonIgnore decorator", () => { - it("Testing JsonIgnore serialization", () => { - class Event { - @JsonProperty() - id: number; - @JsonProperty() - location: string; - @JsonIgnore() - state: string; - - constructor(id: number, location: string, state: string) { - this.id = id; - this.location = location; - this.state = state; - } - } - - const serializedString: String = ObjectMapper.serialize( - new Event(1, "Canberra", "new") - ); - expect(serializedString).toBe('{"id":1,"location":"Canberra"}'); - }); - - it("Testing JsonIgnore deserialization", () => { - class Event { - @JsonProperty() - id: number = undefined; - @JsonProperty() - location: string = undefined; - @JsonIgnore() - state = "old"; - } - - const json = { - id: "1", - location: "Canberra", - state: "new", - }; - - const testInstance: Event = ObjectMapper.deserialize(Event, json); - expect(testInstance.location).toBe("Canberra"); - expect(testInstance.state).toBe("old"); - }); -}); - -describe("Testing JsonIgnore decorator", () => { - it("Testing JsonIgnore serialization", () => { - class Event { - @JsonProperty() - id: number; - @JsonProperty() - location: string; - @JsonIgnore() - state: string; - - constructor(id: number, location: string, state: string) { - this.id = id; - this.location = location; - this.state = state; - } - } - - const serializedString: String = ObjectMapper.serialize( - new Event(1, "Canberra", "new") - ); - expect(serializedString).toBe('{"id":1,"location":"Canberra"}'); - }); - - it("Testing JsonIgnore deserialization", () => { - class Event { - @JsonProperty() - id: number = undefined; - @JsonProperty() - location: string = undefined; - @JsonIgnore() - state = "old"; - } - - const json = { - id: "1", - location: "Canberra", - state: "new", - }; - - const testInstance: Event = ObjectMapper.deserialize(Event, json); - expect(testInstance.location).toBe("Canberra"); - expect(testInstance.state).toBe("old"); - }); - - describe("DeserializationConfig", () => { - it("{ ignoreNameMetadata: true } should use property name", () => { - class Event { - @JsonProperty() - id: number = undefined; - @JsonProperty({ name: "geo-location" }) - location = "old"; - } - - const json = { - id: "1", - location: "Canberra", - }; - - const testInstance: Event = ObjectMapper.deserialize(Event, json, { - ignoreNameMetadata: true, - }); - expect(testInstance.location).toBe("Canberra"); - }); - - it("{ ignoreNameMetadata: true } should use name metadata during serialization", () => { - class Event { - @JsonProperty() - id: number = undefined; - @JsonProperty({ name: "geo-location" }) - location = "old"; - } - - const json = { - id: "1", - location: "Canberra", - }; - - const testInstance: Event = ObjectMapper.deserialize(Event, json, { - ignoreNameMetadata: true, - }); - - const serialized = ObjectMapper.serialize(testInstance); - expect(serialized).toBe('{"id":"1","geo-location":"Canberra"}'); - }); - }); - - it("{ ignoreNameMetadata: true } should work with array serialization", () => { - class DeserializeComplexTypeArrayTest { - @JsonProperty() - storeName: string = undefined; - - @JsonProperty({ name: "AVAILABLE_AT" }) - availableAt: String[] = undefined; - } - - const json = { - storeName: "PizzaHut", - availableAt: ["2000", "3000", "4000", "5000"], - }; - - const testInstance = ObjectMapper.deserialize( - DeserializeComplexTypeArrayTest, - json, - { - ignoreNameMetadata: true, - } - ); - - const serialized = ObjectMapper.serialize(testInstance); - expect(serialized).toBe( - '{"storeName":"PizzaHut","AVAILABLE_AT":["2000","3000","4000","5000"]}' - ); - }); -}); +import "reflect-metadata"; +import { + AccessType, + Deserializer, + JsonIgnore, + JsonProperty, + JsonPropertyDecoratorMetadata, + Serializer +} from "../main/DecoratorMetadata"; +import { getOrCreateDeserializer } from "../main/DeserializationHelper"; +import { ObjectMapper } from "../main/index"; +import { + DateSerializer, + getOrCreateSerializer +} from "../main/SerializationHelper"; +import { a, b } from "./NameSpaces"; + +describe("Testing deserialize functions", () => { + it("Testing Class type with no annotations and 0 children", () => { + class NameTypeWithoutAnnotations { + firstName: string = undefined; + lastName: string = undefined; + middleName: string = undefined; + + public getFirstName(): string { + return this.firstName; + } + } + const nameTypeWithoutAnnotationsJson = { + firstName: "John", + middleName: "P", + lastName: "Doe" + }; + + const processedDto: NameTypeWithoutAnnotations = ObjectMapper.deserialize( + NameTypeWithoutAnnotations, + nameTypeWithoutAnnotationsJson + ); + expect(processedDto.getFirstName()).toBe("John"); + expect(processedDto.middleName).toBe("P"); + expect(processedDto.lastName).toBe("Doe"); + }); + + class Event {} + + class EventsArray { + @JsonProperty({ type: Event }) + eventsArray: Event[] = undefined; + } +}); + +describe("Testing serialize array function", () => { + it("Testing array serialization", () => { + class Event { + id: number = undefined; + location: string = undefined; + constructor(id: number, location: string) { + this.id = id; + this.location = location; + } + } + const eventsArray: Event[] = [ + new Event(1, "Canberra"), + new Event(2, "Sydney"), + new Event(3, "Melbourne") + ]; + + const serializedString: String = ObjectMapper.serialize(eventsArray); + expect(serializedString).toBe( + `[{"id":1,"location":"Canberra"},{"id":2,"location":"Sydney"},{"id":3,"location":"Melbourne"}]` + ); + }); +}); + +describe("Testing deserialize array function", () => { + it("Testing array serialization 1", () => { + class Event { + id: number = undefined; + location: string = undefined; + } + + const json = JSON.parse( + '[{"id":1,"location":"Canberra"},{"id":2,"location":"Sydney"},{"id":3,"location":"Melbourne"}]' + ); + + const eventsArray: Event[] = ObjectMapper.deserializeArray(Event, json); + expect(eventsArray.length > 0); + expect(eventsArray[0].id).toBe(1); + expect(eventsArray[0].location).toBe("Canberra"); + expect(eventsArray[1].id).toBe(2); + expect(eventsArray[1].location).toBe("Sydney"); + expect(eventsArray[2].id).toBe(3); + expect(eventsArray[2].location).toBe("Melbourne"); + }); + + it("Deserialize array with one null value", () => { + class Event { + id: number = undefined; + location: string = undefined; + } + + const json = JSON.parse( + '[{"id":1,"location":"Canberra"},{"id":2,"location":"Sydney"}, {"id":3,"location":"Melbourne"}, null]' + ); + const eventsArray: Event[] = ObjectMapper.deserializeArray(Event, json); + expect(eventsArray.length > 0); + expect(eventsArray[0].id).toBe(1); + expect(eventsArray[0].location).toBe("Canberra"); + expect(eventsArray[1].id).toBe(2); + expect(eventsArray[1].location).toBe("Sydney"); + expect(eventsArray[2].id).toBe(3); + expect(eventsArray[2].location).toBe("Melbourne"); + }); + + it("Testing array serialization 2", () => { + class Friend { + id: number = undefined; + name: string = undefined; + uuid: string = undefined; + age: number = undefined; + email: string = undefined; + gender: string = undefined; + } + + const json = [ + { + id: 0, + name: "Kim Hernandez", + uuid: "5a8f55ea-f667-489a-b29f-13e1e6594963", + age: 20, + email: "kimhernandez@comverges.com", + gender: "female" + }, + { + id: 1, + name: "Sophie Hudson", + uuid: "c61d5c41-e807-4ff1-ae88-19eeb5429411", + age: 33, + email: "sophiehudson@comverges.com", + gender: "female" + }, + { + id: 2, + name: "Rowland Gates", + uuid: "6ce0a1bd-c955-4fb7-a89a-f9cca038de5e", + age: 26, + email: "rowlandgates@comverges.com", + gender: "male" + }, + { + id: 3, + name: "Madeline Ewing", + uuid: "678fa258-2481-4457-965d-5d8571cb59cc", + age: 25, + email: "madelineewing@comverges.com", + gender: "female" + }, + { + id: 4, + name: "Stevens Ryan", + uuid: "8b67b198-7eb9-4bb6-a317-315a22c89c1d", + age: 40, + email: "stevensryan@comverges.com", + gender: "male" + }, + { + id: 5, + name: "Malone Chang", + uuid: "2859f03b-a648-478f-bfd9-f913993dfe74", + age: 34, + email: "malonechang@comverges.com", + gender: "male" + }, + { + id: 6, + name: "Arlene Small", + uuid: "bb3a9e09-4748-47a5-8a9c-ad1c51a43399", + age: 39, + email: "arlenesmall@comverges.com", + gender: "female" + }, + { + id: 7, + name: "Josefa Blackburn", + uuid: "f858dbd4-f4f3-4f0e-9601-854c31fb73bb", + age: 40, + email: "josefablackburn@comverges.com", + gender: "female" + }, + { + id: 8, + name: "Dorothea Lopez", + uuid: "ddce2735-8aa0-4aca-8b6a-42f42bedcc73", + age: 22, + email: "dorothealopez@comverges.com", + gender: "female" + }, + { + id: 9, + name: "Cecile Soto", + uuid: "1fab793a-a691-4185-ba36-8023d961cee7", + age: 40, + email: "cecilesoto@comverges.com", + gender: "female" + }, + { + id: 10, + name: "Barrett Pope", + uuid: "7a4ceca2-95a4-4894-987a-69c86b98a313", + age: 33, + email: "barrettpope@comverges.com", + gender: "male" + }, + { + id: 11, + name: "Eliza Torres", + uuid: "f24351ff-ee2f-4483-b525-fd7cef62d56c", + age: 39, + email: "elizatorres@comverges.com", + gender: "female" + }, + { + id: 12, + name: "Baxter Cannon", + uuid: "ba1d7536-637f-412b-a2ba-0eab28c1b156", + age: 29, + email: "baxtercannon@comverges.com", + gender: "male" + }, + { + id: 13, + name: "Greene Martin", + uuid: "80d4aa47-3e13-4f77-835d-97f030188488", + age: 33, + email: "greenemartin@comverges.com", + gender: "male" + }, + { + id: 14, + name: "Mckinney Lowe", + uuid: "6852df51-efb3-4fdc-baed-4cd90a97fb39", + age: 22, + email: "mckinneylowe@comverges.com", + gender: "male" + }, + { + id: 15, + name: "Chambers Sloan", + uuid: "f1e7a0c0-7620-40a6-84cd-b5578b20cad5", + age: 26, + email: "chamberssloan@comverges.com", + gender: "male" + }, + { + id: 16, + name: "Lynne Gillespie", + uuid: "1d6465a1-2881-4a58-96a8-2e5ccfddfbf4", + age: 33, + email: "lynnegillespie@comverges.com", + gender: "female" + }, + { + id: 17, + name: "Fern York", + uuid: "34e25ab4-e268-468f-bb16-e8a2390fbd72", + age: 34, + email: "fernyork@comverges.com", + gender: "female" + }, + { + id: 18, + name: "Diaz Shelton", + uuid: "896c2c51-a8af-4c42-b069-13d4437a97f3", + age: 30, + email: "diazshelton@comverges.com", + gender: "male" + }, + { + id: 19, + name: "Carol Lindsay", + uuid: "4809bba6-4986-4501-9bc5-0437f22d1e2e", + age: 26, + email: "carollindsay@comverges.com", + gender: "female" + }, + { + id: 20, + name: "Eugenia Day", + uuid: "36e116d2-2d99-4052-a957-9b9a7e75d84f", + age: 38, + email: "eugeniaday@comverges.com", + gender: "female" + }, + { + id: 21, + name: "Marsha Bradford", + uuid: "e57a3802-1173-452c-ace6-d60bf13b7b88", + age: 35, + email: "marshabradford@comverges.com", + gender: "female" + }, + { + id: 22, + name: "Avila Saunders", + uuid: "161919ec-65aa-4875-b7f6-68c684a2f57a", + age: 40, + email: "avilasaunders@comverges.com", + gender: "male" + }, + { + id: 23, + name: "Hartman Herman", + uuid: "b71a026e-7909-4105-af1a-3d314821edba", + age: 20, + email: "hartmanherman@comverges.com", + gender: "male" + }, + { + id: 24, + name: "Yolanda Rodriguez", + uuid: "21ec9746-7c04-42b9-a492-ced69e5203c9", + age: 29, + email: "yolandarodriguez@comverges.com", + gender: "female" + }, + { + id: 25, + name: "Head Nichols", + uuid: "a514f653-c0ac-4028-921e-43bd3c32c14c", + age: 20, + email: "headnichols@comverges.com", + gender: "male" + }, + { + id: 26, + name: "Albert Gardner", + uuid: "a962a277-81fc-425a-b320-e676d957ab06", + age: 24, + email: "albertgardner@comverges.com", + gender: "male" + }, + { + id: 27, + name: "Claudine Wells", + uuid: "4d9d781b-0215-47fa-8c24-720058c2bbec", + age: 21, + email: "claudinewells@comverges.com", + gender: "female" + }, + { + id: 28, + name: "Addie Long", + uuid: "16712b38-99e9-487b-be88-dd9f31206360", + age: 33, + email: "addielong@comverges.com", + gender: "female" + }, + { + id: 29, + name: "Deidre Puckett", + uuid: "f3cf9b5b-f7b6-46cc-8a75-57634c6bf3f8", + age: 34, + email: "deidrepuckett@comverges.com", + gender: "female" + } + ]; + + const friends: Friend[] = ObjectMapper.deserializeArray(Friend, json); + expect(friends.length).toBe(30); + + expect(friends[15].name).toBe("Chambers Sloan"); + expect(friends[29].email).toBe("deidrepuckett@comverges.com"); + expect(friends[25].uuid).toBe("a514f653-c0ac-4028-921e-43bd3c32c14c"); + expect(friends[18].gender).toBe("male"); + }); +}); + +describe("Testing serialize functions", () => { + it("Testing Class type with no annotations and 0 children", () => { + const SimpleClassJson = { + firstName: "John", + middleName: "P", + lastName: "Doe" + }; + + const stringrified: String = ObjectMapper.serialize(SimpleClassJson); + // console.log(JSON.stringify(SimpleClassJson)); + expect(stringrified).toBe( + `{"firstName":"John","middleName":"P","lastName":"Doe"}` + ); + }); + + it("Testing Class type with no annotations and an array", () => { + class SimpleClass { + firstName = "John"; + middleName = "P"; + lastName = "Doe"; + @JsonProperty({ type: String, name: "AKA" }) + knownAs: String[] = ["John", "Doe", "JohnDoe", "JohnPDoe"]; + @JsonProperty({ + type: Date, + name: "dateOfBirth", + serializer: DateSerializer + }) + dob: Date = new Date(1483142400000); // Sat Dec 31, 2016 + } + + const intance: SimpleClass = new SimpleClass(); + + const stringrified: String = ObjectMapper.serialize(intance); + expect(stringrified).toBe( + `{"firstName":"John","middleName":"P","lastName":"Doe","dateOfBirth":1483142400000,"AKA":["John","Doe","JohnDoe","JohnPDoe"]}` + ); + }); + + it("Test all simple type properties", () => { + class SimpleRoster { + private name: String = undefined; + private worksOnWeekend: Boolean = undefined; + private numberOfHours: Number = undefined; + @JsonProperty({ type: Date }) + private systemDate: Date = undefined; + + public isAvailableToday(): Boolean { + if ( + this.systemDate.getDay() % 6 === 0 && + this.worksOnWeekend === false + ) { + return false; + } + return true; + } + } + const json = { + name: "John Doe", + worksOnWeekend: false, + numberOfHours: 8, + systemDate: 1483142400000 // Sat Dec 31, 2016 + }; + + const testInstance: SimpleRoster = ObjectMapper.deserialize( + SimpleRoster, + json + ); + expect(testInstance.isAvailableToday()).toBeFalsy(); + }); +}); + +describe("Testing NameSpaces", () => { + it("Test 1", () => { + const random1 = Math.random(); + const random2 = Date.now(); + + const json = { + c: "This is a test", + d: { + f: random1, + t: random2 + } + }; + + const testInstance = ObjectMapper.deserialize(b.NamespaceBClass, json); + expect(testInstance.d.f).toBe(random1); + expect(testInstance.d.t).toBe(random2); + }); +}); + +describe("Misc tests", () => { + it("Tesing Dto with functions", () => { + class Roster { + private name: string = undefined; + private worksOnWeekend = false; + private today: Date = new Date(); + public isAvailable(date: Date): boolean { + if (date.getDay() % 6 === 0 && this.worksOnWeekend === false) { + return false; + } + return true; + } + public isAvailableToday(): boolean { + return this.isAvailable(this.today); + } + } + + const json = { + name: "John Doe", + worksOnWeekend: false + }; + + const testInstance: Roster = ObjectMapper.deserialize(Roster, json); + expect(testInstance.isAvailable(new Date("2016-12-17"))).toBeFalsy(); + expect(testInstance.isAvailable(new Date("2016-12-16"))).toBeTruthy(); + expect(testInstance.isAvailableToday()).toBe( + new Date().getDay() % 6 === 0 ? false : true + ); + }); + + it("Testing enum array serializing", () => { + enum Days { + Sun, + Mon, + Tues, + Wed, + Thurs, + Fri, + Sat + } + + class DaysEnumArraySerializer implements Serializer { + serialize = (values: Days[]): any => { + let result: string[] = values.map((value: Days) => Days[value]); + return JSON.stringify(result); + }; + } + + class Calendar { + month: string = undefined; + @JsonProperty({ type: Days, serializer: DaysEnumArraySerializer }) + days: Days[] = undefined; + } + + let cal: Calendar = new Calendar(); + cal.month = "June"; + cal.days = [Days.Mon, Days.Tues]; + const serialized: String = ObjectMapper.serialize(cal); + expect(serialized).toBe(`{"month":"June","days":["Mon","Tues"]}`); + }); + + it("Testing enum ", () => { + class DaysEnumSerializerDeserializer implements Deserializer, Serializer { + deserialize = (value: string): Days => { + return Days[value]; + }; + serialize = (value: Days): string => { + return `"${Days[value]}"`; + }; + } + + enum Days { + Sun, + Mon, + Tues, + Wed, + Thurs, + Fri, + Sat + } + + class Workday { + @JsonProperty({ + type: Days, + deserializer: DaysEnumSerializerDeserializer, + serializer: DaysEnumSerializerDeserializer + }) + today: Days = undefined; + } + + const json = { today: "Tues" }; + + const testInstance: Workday = ObjectMapper.deserialize(Workday, json); + expect(testInstance.today === Days.Tues).toBeTruthy(); + testInstance.today = Days.Fri; + const serialized: String = ObjectMapper.serialize(testInstance); + expect(serialized).toBe(`{"today":"Fri"}`); + }); + + it("Testing AccessType.READ_ONLY", () => { + class SimpleClass { + firstName = "John"; + middleName = "P"; + lastName = "Doe"; + @JsonProperty({ type: String, name: "AKA", access: AccessType.READ_ONLY }) + knownAs: String[] = ["John", "Doe", "JohnDoe", "JohnPDoe"]; + } + + const intance: SimpleClass = new SimpleClass(); + + const stringrified: String = ObjectMapper.serialize(intance); + expect(stringrified).toBe( + `{"firstName":"John","middleName":"P","lastName":"Doe"}` + ); + }); + + it("Testing AccessType.WRITE_ONLY", () => { + class Roster { + @JsonProperty({ access: AccessType.WRITE_ONLY }) + private name: string = undefined; + private worksOnWeekend = false; + private today: Date = new Date(); + public isAvailable(date: Date): boolean { + if (date.getDay() % 6 === 0 && this.worksOnWeekend === false) { + return false; + } + return true; + } + public isAvailableToday(): boolean { + return this.isAvailable(this.today); + } + public getName(): String { + return this.name; + } + } + + const json = { + name: "John Doe", + worksOnWeekend: false + }; + + const testInstance: Roster = ObjectMapper.deserialize(Roster, json); + expect(testInstance.getName()).toBeUndefined(); + }); + + it("Testing deserializer and serializer instances", () => { + class TestSerailizer implements Serializer { + serialize = (value: any): any => {}; + } + + const instance1: TestSerailizer = getOrCreateSerializer(TestSerailizer); + const instance2: TestSerailizer = getOrCreateSerializer(TestSerailizer); + expect(instance1).toBe(instance2); + + class TestDeserializer implements Deserializer { + deserialize = (value: any): any => {}; + } + + const instance3: TestDeserializer = getOrCreateDeserializer( + TestDeserializer + ); + const instance4: TestDeserializer = getOrCreateDeserializer( + TestDeserializer + ); + expect(instance3).toBe(instance4); + }); +}); + +describe("Testing JsonIgnore decorator", () => { + it("Testing JsonIgnore serialization", () => { + class Event { + @JsonProperty() + id: number; + @JsonProperty() + location: string; + @JsonIgnore() + state: string; + + constructor(id: number, location: string, state: string) { + this.id = id; + this.location = location; + this.state = state; + } + } + + const serializedString: String = ObjectMapper.serialize( + new Event(1, "Canberra", "new") + ); + expect(serializedString).toBe('{"id":1,"location":"Canberra"}'); + }); + + it("Testing JsonIgnore deserialization", () => { + class Event { + @JsonProperty() + id: number = undefined; + @JsonProperty() + location: string = undefined; + @JsonIgnore() + state = "old"; + } + + const json = { + id: "1", + location: "Canberra", + state: "new" + }; + + const testInstance: Event = ObjectMapper.deserialize(Event, json); + expect(testInstance.location).toBe("Canberra"); + expect(testInstance.state).toBe("old"); + }); +}); + +describe("Testing JsonIgnore decorator", () => { + it("Testing JsonIgnore serialization", () => { + class Event { + @JsonProperty() + id: number; + @JsonProperty() + location: string; + @JsonIgnore() + state: string; + + constructor(id: number, location: string, state: string) { + this.id = id; + this.location = location; + this.state = state; + } + } + + const serializedString: String = ObjectMapper.serialize( + new Event(1, "Canberra", "new") + ); + expect(serializedString).toBe('{"id":1,"location":"Canberra"}'); + }); + + it("Testing JsonIgnore deserialization", () => { + class Event { + @JsonProperty() + id: number = undefined; + @JsonProperty() + location: string = undefined; + @JsonIgnore() + state = "old"; + } + + const json = { + id: "1", + location: "Canberra", + state: "new" + }; + + const testInstance: Event = ObjectMapper.deserialize(Event, json); + expect(testInstance.location).toBe("Canberra"); + expect(testInstance.state).toBe("old"); + }); + + describe("DeserializationConfig", () => { + it("{ ignoreNameMetadata: true } should use property name", () => { + class Event { + @JsonProperty() + id: number = undefined; + @JsonProperty({ name: "geo-location" }) + location = "old"; + } + + const json = { + id: "1", + location: "Canberra" + }; + + const testInstance: Event = ObjectMapper.deserialize(Event, json, { + ignoreNameMetadata: true + }); + expect(testInstance.location).toBe("Canberra"); + }); + + it("{ ignoreNameMetadata: true } should use name metadata during serialization", () => { + class Event { + @JsonProperty() + id: number = undefined; + @JsonProperty({ name: "geo-location" }) + location = "old"; + } + + const json = { + id: "1", + location: "Canberra" + }; + + const testInstance: Event = ObjectMapper.deserialize(Event, json, { + ignoreNameMetadata: true + }); + + const serialized = ObjectMapper.serialize(testInstance); + expect(serialized).toBe('{"id":"1","geo-location":"Canberra"}'); + }); + }); + + it("{ ignoreNameMetadata: true } should work with array serialization", () => { + class DeserializeComplexTypeArrayTest { + @JsonProperty() + storeName: string = undefined; + + @JsonProperty({ name: "AVAILABLE_AT" }) + availableAt: String[] = undefined; + } + + const json = { + storeName: "PizzaHut", + availableAt: ["2000", "3000", "4000", "5000"] + }; + + const testInstance = ObjectMapper.deserialize( + DeserializeComplexTypeArrayTest, + json, + { + ignoreNameMetadata: true + } + ); + + const serialized = ObjectMapper.serialize(testInstance); + expect(serialized).toBe( + '{"storeName":"PizzaHut","AVAILABLE_AT":["2000","3000","4000","5000"]}' + ); + }); + + it("{ ignoreDeserialization: true } should not use specified deserializer", () => { + class StringToNumberDeserializer implements Deserializer { + deserialize(value: string) { + return Number.parseFloat(value); + } + } + + class Event { + @JsonProperty() + id: number = undefined; + @JsonProperty({ deserializer: StringToNumberDeserializer }) + location = undefined; + } + + const json = { + id: "1", + location: "125.55" + }; + + const testInstance: Event = ObjectMapper.deserialize(Event, json, { + ignoreDeserializer: true + }); + + const serialized = ObjectMapper.serialize(testInstance); + expect(serialized).toBe('{"id":"1","location":"125.55"}'); + }); +}); From be68961cb09bab25536d7de44dcd600416cf389e Mon Sep 17 00:00:00 2001 From: Shakil Siraj Date: Tue, 12 Aug 2025 13:30:22 +1000 Subject: [PATCH 2/2] Release 1.9.0 --- README.md | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c27d7e2..fb2cfc0 100644 --- a/README.md +++ b/README.md @@ -168,7 +168,7 @@ aexpect(serialized).toBe('{"id":"1","geo-location":"Canberra"}'); ``` ### Ignoring special deserializers all together -If you have an use case where you want to not use specified deserializer in the metadata using de-serialization, the you can turn if off by setting `ignoreNameMetadata` to true (default is false). Here is an example usage: +If you have an use case where you want to not use specified deserializer in the metadata using de-serialization, you can turn if off by setting `ignoreDeserializer` to true (default is false). Here is an example usage: ```typescript class StringToNumberDeserializer implements Deserializer { diff --git a/package-lock.json b/package-lock.json index f08f65f..6e2919d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "json-object-mapper", - "version": "1.9.0-rc1", + "version": "1.9.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "json-object-mapper", - "version": "1.9.0-rc1", + "version": "1.9.0", "license": "MIT", "devDependencies": { "@types/es6-shim": "^0.31.42", diff --git a/package.json b/package.json index dc58b14..6e086c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "json-object-mapper", - "version": "1.9.0-rc1", + "version": "1.9.0", "description": "A TypeScript library to serialize and deserialize JSON objects in a fast and non-recursive way", "repository": { "type": "git",