Skip to content

Commit

Permalink
Add @toJson decorator
Browse files Browse the repository at this point in the history
  • Loading branch information
Neos3452 committed Mar 14, 2019
1 parent 60447ee commit 7c84588
Show file tree
Hide file tree
Showing 10 changed files with 198 additions and 3 deletions.
1 change: 1 addition & 0 deletions js/typedjson.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { jsonMember } from "./typedjson/json-member";
export { jsonArrayMember } from "./typedjson/json-array-member";
export { jsonSetMember } from "./typedjson/json-set-member";
export { jsonMapMember } from "./typedjson/json-map-member";
export { toJson } from "./typedjson/to-json";
24 changes: 24 additions & 0 deletions js/typedjson.js

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion js/typedjson.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/typedjson.min.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/typedjson.min.js.map

Large diffs are not rendered by default.

22 changes: 22 additions & 0 deletions js/typedjson/to-json.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Options for the @toJson decorator.
*/
export interface IToJsonOptions {
/**
* When set to true it will overwrite any toJSON already existing on the prototype.
*/
overwrite?: boolean;
}
/**
* Decorator that will generate toJSON function on the class prototype that allows
* JSON.stringify to be used instead of TypedJSON.stringify. Under the hood it will
* simply delegate to TypedJSON.
* By default it will throw if the prototype already has a toJSON function defined.
* @param target the class which prototype should be modified.
*/
export declare function toJson<T extends Object>(target: Function): void;
/**
* Decorator factory that accepts the options interface.
* @param options for configuring the toJSON creation.
*/
export declare function toJson<T extends Object>(options: IToJsonOptions): ((target: Function) => void);
4 changes: 4 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,10 @@ class MyDataClass

This is not needed for `@jsonArrayMember`, `@jsonMapMember`, and `@jsonSetMember`, as those types already know the property type itself, as well as element/key types (although using ReflectDecorators adds runtime-type checking to these decorators, to help you spot errors).

### Using `JSON.stringify`

If you want to use `JSON.stringify` to serialize the objects using TypedJSON you can annotate a class with `@toJson` and it will create `toJSON` function on the class prototype. By default it will throw an error if such function is already defined, but you can override this behavior by setting `overwrite` to `true` in the decorator's options.

## Limitations

### Type-definitions
Expand Down
97 changes: 97 additions & 0 deletions spec/to-json.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import {jsonObject, jsonMember, toJson} from "../js/typedjson";

describe('toJson decorator', function () {
it('should work with JSON.stringify', function () {
@toJson
@jsonObject
class Person {
firstName?: string;

@jsonMember({name: 'surname'})
lastName?: string;

public getFullName() {
return this.firstName + " " + this.lastName;
}
}

const person = new Person;
person.firstName = 'John';
person.lastName = 'Doe';
expect(JSON.stringify(person)).toBe('{"surname":"Doe"}');
});

it('should work on the abstract class', function () {
@toJson
abstract class Base {
@jsonMember({name: 'renamed'})
prop?: string;
}

@jsonObject
class Sub extends Base {
@jsonMember({name: 'numeric'})
num?: number;
}

@jsonObject
class OtherSub extends Base {
@jsonMember
decimal?: number;
ignored?: string;
}


const sub = new Sub;
sub.prop = 'value';
sub.num = 20;
expect(JSON.stringify(sub)).toBe('{"renamed":"value","numeric":20}');

const otherSub = new OtherSub;
otherSub.prop = 'value';
otherSub.decimal = 123;
otherSub.ignored = 'assigned';
expect(JSON.stringify(otherSub)).toBe('{"renamed":"value","decimal":123}');
});

it("should throw an error when toJSON already exists", function () {
try {
@toJson
@jsonObject
class Some {
@jsonMember
prop?: string;

toJSON() {
return {};
}
}

const some = new Some;
some.prop = 'value';
expect(JSON.stringify(some)).toBe('{}');

fail('Should not succeed');
} catch (error) {
// ok
}
});


it("should overwrite toJSON when overwrite is true", function () {
@toJson({overwrite: true})
@jsonObject
class Some {
@jsonMember
prop?: string;

toJSON() {
return {};
}
}

const some = new Some;
some.prop = 'value';
expect(JSON.stringify(some)).toBe('{"prop":"value"}');
});
});
1 change: 1 addition & 0 deletions src/typedjson.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export { jsonMember } from "./typedjson/json-member";
export { jsonArrayMember } from "./typedjson/json-array-member";
export { jsonSetMember } from "./typedjson/json-set-member";
export { jsonMapMember } from "./typedjson/json-map-member";
export { toJson } from "./typedjson/to-json";
46 changes: 46 additions & 0 deletions src/typedjson/to-json.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { TypedJSON } from "../parser";

/**
* Options for the @toJson decorator.
*/
export interface IToJsonOptions {
/**
* When set to true it will overwrite any toJSON already existing on the prototype.
*/
overwrite?: boolean;
}

/**
* Decorator that will generate toJSON function on the class prototype that allows
* JSON.stringify to be used instead of TypedJSON.stringify. Under the hood it will
* simply delegate to TypedJSON.
* By default it will throw if the prototype already has a toJSON function defined.
* @param target the class which prototype should be modified.
*/
export function toJson<T extends Object>(target: Function): void;
/**
* Decorator factory that accepts the options interface.
* @param options for configuring the toJSON creation.
*/
export function toJson<T extends Object>(options: IToJsonOptions): ((target: Function) => void);
export function toJson<T extends Object>(optionsOrTarget: IToJsonOptions | Function
): ((target: Function) => void) | void {
if (typeof optionsOrTarget === 'function') {
// used directly
toJsonDecorator(optionsOrTarget, {});
return;
}
// used as a factory
return (target: Function) => {
toJsonDecorator(target, optionsOrTarget);
}
}

function toJsonDecorator<T extends Object>(target: Function, options: IToJsonOptions): void {
if (!options.overwrite && target.prototype.toJSON) {
throw new Error(`${target.name} already has toJSON defined!`);
}
target.prototype.toJSON = function () {
return TypedJSON.toPlainJson(this, Object.getPrototypeOf(this).constructor);
}
}

0 comments on commit 7c84588

Please sign in to comment.