Skip to content

Commit

Permalink
feat: Add passUnknownProperties option for serialization.
Browse files Browse the repository at this point in the history
- Added `passUnknownProperties` option to `DeserializeOptions` and `SerializeOptions`
- Modified `mapObjectProperty` function to handle the new option
- Updated `DeserializeObject` and `SerializeObject` functions to pass the options to `mapObjectProperty`
- Added tests for the new option in both deserialization and serialization scenarios
  • Loading branch information
Unnoen committed Oct 17, 2023
1 parent fb21316 commit afa6748
Show file tree
Hide file tree
Showing 4 changed files with 182 additions and 11 deletions.
41 changes: 33 additions & 8 deletions src/features.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import {
type DeserializeOptions,
type IJsonClassMetadata,
type IJsonPropertyMetadata,
JsonType,
PropertyNullability,
type SerializeOptions,
} from './types';

export const metadataKey = Symbol('UntypedJSONMetadata');
Expand Down Expand Up @@ -107,9 +109,10 @@ const verifyType = (propertyKey: string, properties: IJsonPropertyMetadata, json
* @param {any} fromObject The object to map from.
* @param {any} toObject The object to map to.
* @param {boolean} serialize Whether to serialize or deserialize.
* @param {DeserializeOptions | SerializeOptions} options The options for deserialization or serialization.
* @returns {void}
*/
const mapObjectProperty = (propertyKey: string, properties: IJsonPropertyMetadata, fromObject: any, toObject: any, serialize: boolean = false): void => {
const mapObjectProperty = (propertyKey: string, properties: IJsonPropertyMetadata, fromObject: any, toObject: any, serialize: boolean = false, options?: DeserializeOptions | SerializeOptions): void => {
const {
jsonProperty,
array,
Expand All @@ -133,10 +136,10 @@ const mapObjectProperty = (propertyKey: string, properties: IJsonPropertyMetadat
if (nested) {
if (array) {
toObject[toKey] = fromObject[fromKey].map((item: any) => {
return serialize ? SerializeObject(item) : DeserializeObject(item, classType);
return serialize ? SerializeObject(item, options) : DeserializeObject(item, classType, options);
});
} else {
toObject[toKey] = serialize ? SerializeObject(fromObject[fromKey]) : DeserializeObject(fromObject[fromKey], classType);
toObject[toKey] = serialize ? SerializeObject(fromObject[fromKey], options) : DeserializeObject(fromObject[fromKey], classType, options);
}
} else if (array) {
if (classType === undefined) {
Expand Down Expand Up @@ -165,11 +168,12 @@ const mapObjectProperty = (propertyKey: string, properties: IJsonPropertyMetadat
* @template T
* @param {object | string} json The JSON object to deserialize. Can be a string or an object.
* @param {T} classReference The class reference to deserialize the JSON object into.
* @param {DeserializeOptions} options The options for deserialization.
* @returns {T} The deserialized instance of the class.
* @throws {TypeError} If the JSON object is not an object or string.
* @throws {ReferenceError} If the property is not defined in the JSON.
*/
export const DeserializeObject = <T>(json: object | string, classReference: new() => T): T => {
export const DeserializeObject = <T>(json: object | string, classReference: new() => T, options?: DeserializeOptions): T => {
const jsonObject: object = typeof json === 'string' ? JSON.parse(json) : json;

if (typeof jsonObject !== 'object') {
Expand All @@ -189,7 +193,7 @@ export const DeserializeObject = <T>(json: object | string, classReference: new(
});

for (const mixin of classMetadata.mixins) {
const mixinObject = DeserializeObject(jsonObject, mixin);
const mixinObject = DeserializeObject(jsonObject, mixin, options);
Object.assign(instance, mixinObject);

const mixinKeys = Object.getOwnPropertyNames(mixin.prototype);
Expand All @@ -206,7 +210,17 @@ export const DeserializeObject = <T>(json: object | string, classReference: new(
for (const propertyKey of propertyKeys) {
verifyNullability(propertyKey, classMetadata.properties[propertyKey], jsonObject);
verifyType(propertyKey, classMetadata.properties[propertyKey], jsonObject);
mapObjectProperty(propertyKey, classMetadata.properties[propertyKey], jsonObject, instance);
mapObjectProperty(propertyKey, classMetadata.properties[propertyKey], jsonObject, instance, false, options);
}

if (options?.passUnknownProperties) {
const unknownProperties = Object.keys(jsonObject).filter((key) => {
return !propertyKeys.includes(key);
});

for (const unknownProperty of unknownProperties) {
instance[unknownProperty] = jsonObject[unknownProperty];
}
}

classConstructor = Object.getPrototypeOf(classConstructor);
Expand All @@ -219,9 +233,10 @@ export const DeserializeObject = <T>(json: object | string, classReference: new(
* Serializes an instance of a class into a JSON object.
* @template T
* @param {T} instance The instance of the class to serialize.
* @param {SerializeOptions} options The options for serialization.
* @returns {object} The serialized JSON object.
*/
export const SerializeObject = <T>(instance: T): object => {
export const SerializeObject = <T>(instance: T, options?: SerializeOptions): object => {
const json: object = {};

let classConstructor = instance.constructor;
Expand All @@ -237,7 +252,17 @@ export const SerializeObject = <T>(instance: T): object => {
const propertyKeys = Object.keys(classMetadata?.properties);

for (const propertyKey of propertyKeys) {
mapObjectProperty(propertyKey, classMetadata.properties[propertyKey], instance, json, true);
mapObjectProperty(propertyKey, classMetadata.properties[propertyKey], instance, json, true, options);
}

if (options?.passUnknownProperties && classConstructor === instance.constructor) {
const unknownProperties = Object.keys(instance).filter((key) => {
return !propertyKeys.includes(key);
});

for (const unknownProperty of unknownProperties) {
json[unknownProperty] = instance[unknownProperty];
}
}

classConstructor = Object.getPrototypeOf(classConstructor);
Expand Down
16 changes: 16 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ import {
type JsonConverter,
} from './converters';

/**
* Represents the options for deserializing an object.
* @property {boolean} [passUnknownProperties=false] - Determines whether unknown properties should be passed or ignored during deserialization from JSON.
*/
export type DeserializeOptions = {
passUnknownProperties?: boolean,
};

/**
* Options for serializing objects.
* @property {boolean} [passUnknownProperties=false] - Determines whether unknown properties should be passed or ignored during serialization to JSON.
*/
export type SerializeOptions = {
passUnknownProperties?: boolean,
};

/**
* The type the JSON property is deserialized to.
* This can be a primitive type using JsonType, a class, an array of either, or a custom JsonConverter.
Expand Down
4 changes: 1 addition & 3 deletions test/converters.test.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
import {
type JsonType,
} from '../src';
import {
DeserializeObject,
JsonConverter,
JsonProperty,
type JsonType,
SerializeObject,
} from '../src';

Expand Down
132 changes: 132 additions & 0 deletions test/main.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -442,3 +442,135 @@ describe('JsonProperty Nullability Tests', () => {
}).toThrow('Property t is not defined in the JSON.\r\n{}');
});
});

describe('DeserializeOptions Tests', () => {
it('should pass unknown properties if passUnknownProperties is true', () => {
class TestClass {
@JsonProperty('t', JsonType.STRING)
public test: string;
}

const testJson = DeserializeObject({
t: 'test',
test2: 'test2',
}, TestClass, {
passUnknownProperties: true,
});

expect(testJson.test).toBe('test');
expect((testJson as any).test2).toBe('test2');
});

it('should pass unknown properties of all child classes if passUnknownProperties is true', () => {
class Child {
@JsonProperty('t', JsonType.STRING)
public test: string;
}

class Parent {
@JsonProperty('c', Child)
public child: Child;
}

const testJson = DeserializeObject({
c: {
t: 'test',
test2: 'test2',
},
}, Parent, {
passUnknownProperties: true,
});

expect(testJson.child.test).toBe('test');
expect((testJson.child as any).test2).toBe('test2');
});

it('should not pass unknown properties if passUnknownProperties is false or not passed', () => {
class TestClass {
@JsonProperty('t', JsonType.STRING)
public test: string;
}

const testJson = DeserializeObject({
t: 'test',
test2: 'test2',
}, TestClass, {
passUnknownProperties: false,
});

expect(testJson.test).toBe('test');
expect((testJson as any).test2).toBeUndefined();

class TestClass2 {
@JsonProperty('test', JsonType.STRING)
public test: string;
}

const testJson2 = DeserializeObject({
test: 'test',
test2: 'test2',
}, TestClass2);

expect(testJson2.test).toBe('test');
expect((testJson2 as any).test2).toBeUndefined();
});
});

describe('SerializeOptions Tests', () => {
it('should pass unknown properties if passUnknownProperties is true', () => {
class TestClass {
@JsonProperty('t', JsonType.STRING)
public test: string;
}

const testJson = new TestClass();
testJson.test = 'test';
(testJson as any).test2 = 'test2';

expect(JSON.stringify(SerializeObject(testJson, {
passUnknownProperties: true,
}))).toBe('{"t":"test","test2":"test2"}');
});

it('should pass unknown properties of all child classes if passUnknownProperties is true', () => {
class Child {
@JsonProperty('t', JsonType.STRING)
public test: string;
}

class Parent {
@JsonProperty('c', Child)
public child: Child;
}

const testJson = new Parent();
testJson.child = new Child();
testJson.child.test = 'test';
(testJson.child as any).test2 = 'test2';

expect(JSON.stringify(SerializeObject(testJson, {
passUnknownProperties: true,
}))).toBe('{"c":{"t":"test","test2":"test2"}}');
});

it('should not pass unknown properties if passUnknownProperties is false or not passed', () => {
class TestClass {
@JsonProperty('t', JsonType.STRING)
public test: string;
}

const testJson = new TestClass();
testJson.test = 'test';
(testJson as any).test2 = 'test2';

expect(JSON.stringify(SerializeObject(testJson, {
passUnknownProperties: false,
}))).toBe('{"t":"test"}');

const testJson2 = new TestClass();
testJson2.test = 'test';
(testJson2 as any).test2 = 'test2';

expect(JSON.stringify(SerializeObject(testJson2))).toBe('{"t":"test"}');
});
});

0 comments on commit afa6748

Please sign in to comment.