GithubHelp home page GithubHelp logo

edcarroll / ta-json Goto Github PK

View Code? Open in Web Editor NEW
67.0 6.0 16.0 97 KB

Type-aware JSON serializer/parser

License: MIT License

TypeScript 100.00%
es7 decorators json classes typescript javascript serializer parser es2016

ta-json's Introduction

Type-Aware JSON Parser & Serializer (ta-json)

npm Travis CI

Strongly typed JSON parser & serializer for TypeScript / ES7 via decorators.

Supports parameterized class constructors, nesting classes, inheritance, Arrays and Sets, custom property converters and more.

Installation

$ npm install --save ta-json

Quickstart

Import the necessary decorators and the JSON object from the library, and set up your class.

import { JSON, JsonObject, JsonProperty } from "ta-json";

@JsonObject()
export class Person {
    @JsonProperty()
    public firstName:string;

    @JsonProperty()
    public lastName:string;

    public get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }

    // Note this parameterized constructor
    constructor(fullName:string) {
        let [firstName, lastName] = fullName.split(" ");
        this.firstName = firstName;
        this.lastName = lastName;
    }
}

Parse and stringify with the provided JSON class. Note that you can use this class to work with untyped objects as usual.

let person = new Person("Edward Carroll");
JSON.stringify(person); // {"firstName":"Edward","lastName":"Carroll"}

let fromJson = JSON.parse<Person>('{"firstName":"Edward","lastName":"Carroll"}', Person);
person instanceof Person; // true
person.fullName; // Edward Carroll

For more advanced usage please read the docs below for each of the available decorators.

Overriding the JSON object

This library doesn't make any changes to the global scope, it merely exports its main class with the same name, allowing it to be a drop in replacement for JSON (without any decorators, the library falls back to default functionality).

If you'd prefer to import the library with a different name, you can also import TaJson, which will give you the same class.

import { TaJson } from "ta-json";

Decorators

@JsonObject()

Registers the class with the serializer. Classes don't need to have parameterless constructors to be serialized and parsed - however this means that the internal state of a class must be fully represented by its serialized fields.

If you would like to run functions before and after deserialization, please see @BeforeDeserialized() and @OnDeserialized()

Usage

import { JsonObject } from "ta-json";

@JsonObject()
export class Person {}

@JsonProperty(serializedName?:string)

Properties are mapped on an opt-in basis. Only properties that are decorated are serialized. The name for the property in the serialized document can optionally be specified using this decorator.

Usage

import { JsonObject, JsonProperty } from "ta-json";

@JsonObject()
export class Person {
    @JsonProperty("fullName")
    public name:string;

    @JsonProperty()
    public get firstName() {
        return this.name.split(" ").shift();
    }
}

@JsonType(type:Function)

Specifies the type to be used when serializing a property. Useful when you want to serialize the field using a different type than the type of the property itself, for example an entity reference.

Usage

import { JsonObject, JsonProperty, JsonType } from "ta-json";

@JsonObject()
export class Person {
    @JsonProperty()
    @JsonType(String)
    public name:string;
}

@JsonElementType(type:Function)

Specifies the type to be used when serializing elements of an Array or Set. This is a required decorator when working with these types due to how metadata reflection works.

Usage

import { JsonObject, JsonProperty, JsonElementType } from "ta-json";

@JsonObject()
export class LotteryDraw {
    @JsonProperty()
    @JsonElementType(Number)
    public numbers:Set<number>;
}

@JsonDiscrimatorProperty(property:string) & @JsonDiscriminatorValue(value:any)

These decorators are used when you want to deserialize documents while respecting the class inheritance hierarchy. The discriminator property is used to determine the type of the document, and the descriminator value is set on each subclass (or deeper subclasses) so the document can be matched to the appropriate class.

Multi-level inheritance is fully supported, by the @JsonDiscriminatorValue and the @JsonDiscriminatorProperty decorators being applied to the same class.

Usage

import { JSON, JsonObject, JsonProperty, JsonDiscriminatorProperty, JsonDiscriminatorValue } from "ta-json";

export enum AnimalType { Cat = 0, Dog = 1 }

@JsonObject()
@JsonDiscriminatorProperty("type")
export class Animal {
    @JsonProperty()
    type:AnimalType;
}

@JsonObject()
@JsonDiscriminatorValue(AnimalType.Cat)
export class Cat extends Animal {
    constructor() {
        super();
        this.type = AnimalType.Cat;
    }
}

@JsonObject()
@JsonDiscriminatorValue(AnimalType.Dog)
export class Dog extends Animal {
    constructor() {
        super();
        this.type = AnimalType.Dog;
    }
}

let animals = [new Cat(), new Dog()];

JSON.stringify(animals); // [{"type":0},{"type":1}]
JSON.parse<Animal[]>('[{"type":0},{"type":1}]', Animal); // [ Cat { type: 0 }, Dog { type: 1 } ]

@BeforeDeserialized()

Specifies the method to run before a document has been deserialized into a class, but after the class has been instantiated. This is useful for setting default values that may be overwritten by the deserialization.

Usage

import { JsonObject, JsonProperty, BeforeDeserialized } from "ta-json";

@JsonObject()
export class Demo {
    @JsonProperty()
    public serialized:string;

    @BeforeDeserialized()
    public setDefaults() {
        this.serialized = "default value";
    }
}

@OnDeserialized()

Specifies the method to run once a document has been deserialized into a class. This is useful for example when recalculating private members that aren't serialized into JSON.

Usage

import { JsonObject, JsonProperty, OnDeserialized } from "ta-json";

@JsonObject()
export class Demo {
    private _unserialized:string;

    @JsonProperty()
    public serialized:string;

    @OnDeserialized()
    public onDeserialized() {
        this._unserialized = doOperation(this.serialized);
    }
}

@JsonConstructor()

Specifies the method to be optionally run before a document has been deserialized. The specified method is only run when runConstructor is set to true in the parse options.

Usage

import { JSON, JsonObject, JsonProperty, JsonConstructor } from "ta-json";

@JsonObject()
export class Demo {
    @JsonProperty()
    public example:string;

    constructor(example:string) {
        this.defaultValues(example);
    }

    @JsonConstructor()
    private defaultValues(example:string = "default") {
        this.example = example;
    }
}

JSON.parse<Demo>('{}', Demo, { runConstructor: true }); // Demo { example: 'default' }
JSON.parse<Demo>('{"example":"different"}', Demo, { runConstructor: true }) // Demo { example: 'different' }
JSON.parse<Demo>('{}', Demo); // Demo {}

@JsonConverter(converter:IPropertyConverter | ParameterlessConstructor)

Property converters can be used to define how a type is serialized / deserialized. They must implement the IPropertyConverter interface, and output a JsonValue.

There are two built in converters, DateConverter and BufferConverter. They are applied automatically when serializing Date and Buffer objects.

Example

This example uses the built in BufferConverter, to output Buffer values as base64 encoded strings. Note that when parsing documents, the deserializer will convert the value back into a Buffer.

import { JSON, JsonObject, JsonProperty, JsonConverter, BufferConverter } from "ta-json";

@JsonObject()
export class ConverterDemo {
    @JsonProperty()
    @JsonConverter(new BufferConverter("base64"))
    public bufferValue:Buffer;

    constructor(value:string) {
        this.bufferValue = Buffer.from(value);
    }
}

let demo = new ConverterDemo("hello, world!");
JSON.stringify(demo); // {"bufferValue":"aGVsbG8sIHdvcmxkIQ=="}
let parsed = JSON.parse<ConverterDemo>('{"bufferValue":"aGVsbG8sIHdvcmxkIQ=="}', ConverterDemo);
parsed.bufferValue instanceof Buffer; // true
parsed.bufferValue.toString(); // hello, world!

Usage

Below we define a converter that reverses any string value it is given.

import { JSON, JsonObject, JsonProperty, JsonConverter, IPropertyConverter } from "ta-json";

export class ReverseStringConverter implements IPropertyConverter {
    public serialize(property:string):string {
        return property.split('').reverse().join('');
    }

    public deserialize(value:string):string {
        return value.split('').reverse().join('');
    }
}

@JsonObject()
export class Demo {
    @JsonProperty()
    @JsonConverter(ReverseStringConverter)
    public example:string;
}

let d = new Demo();
d.example = "hello";
JSON.stringify(d); // {"example":"olleh"}

Note that you can also provide an instance of a property converter, for example if you want to customize the output. (This is how the BufferConverter chooses a string encoding).

@JsonReadonly()

The use of this decorator stops the property value being read from the document by the deserializer.

Usage

import { JSON, JsonObject, JsonProperty, JsonReadonly } from "ta-json";

@JsonObject()
export class Person {
    @JsonProperty()
    @JsonReadonly()
    public name:string;
}

JSON.parse<Person>('{"name":"Edward"}', Person).name; // undefined

@JsonWriteonly()

The use of this decorator stops the property value being written to the document by the serializer. Useful for password fields for example.

Usage

import { JSON, JsonObject, JsonProperty, JsonReadonly } from "ta-json";

@JsonObject()
export class User {
    @JsonProperty()
    @JsonWriteonly()
    public password:string;
}

let u = new User();
u.password = "p4ssw0rd";

JSON.stringify(u); // {}
JSON.parse<User>('{"password":"p4ssw0rd"}', User).password; // p4ssw0rd

API

JSON (can also be imported with TaJson)

#stringify(value:any):string

Serializes an object or array into a JSON string. If type definitions aren't found for a given object it falls back to global.JSON.stringify(value).

#parse(json:string, type?:Function, options?:IParseOptions):T

Parses a JSON string into an instance of a class. the type parameter specifies which class to instantiate; however this is an optional parameter and as with #stringify it will fall back to global.JSON.parse(json).

IParseOptions
  • runConstructor:boolean - specifies whether the method decorated with @JsonConstructor() is run upon class initialisation. Default false

#serialize(value:any):any

Serializes an object or array into a JsonValue. This is an intermediary step; i.e. global.JSON.stringify can be called on the returned object to get a JSON string. This function is useful when returning from inside an express (o.e) middleware.

#deserialize(object:any, type?:Function, options?:IParseOptions):T

Similarly to the above, this function can be run on objects produced by global.JSON.parse, returning the same output as #parse. This function is useful in combination with body parsing modules, where the raw JSON has already been parsed into an object.

ta-json's People

Contributors

edcarroll avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

ta-json's Issues

@JsonConverter with number

Is it really possible to use the converter with a string as shown in your example?
I'm asking because of these lines:

const primitive = definition.type === String || definition.type === Boolean || definition.type === Number;

and

   if (!primitive) {
        const converter = definition.converter || propertyConverters.get(definition.type);
        const objDefinition = objectDefinitions.get(definition.type);

        if (converter) {
            return converter.deserialize(value);
        }

        if (objDefinition) {
            return deserialize(value, definition.type);
        }
    }

In my case the type is a number and as a result primitive is true and the converter never executed.
The ReverseStringConverter is used without a 'new' in the example. Is that correct?

Deserialize a null value in a list

Great library!

If I deserialize an array with a null value, I have an error

Uncaught TypeError: Cannot read property 'id' of null
    at eval (deserialize.js:32)

Could you check value before deserialize it ?

Maybe like this

function deserializeObject(object, definition, options) {
	var primitive = definition.type === String || definition.type === Boolean || definition.type === Number;
	var value = object;

	if(value != null)
	{
		var converter = definition.converter || propertyConverters.get(definition.type);
		if (converter) {
			return converter.deserialize(value);
		}
		if (!primitive) {
			var objDefinition = objectDefinitions.get(definition.type);
			if (objDefinition) {
				return deserialize(value, definition.type);
			}
		}
	}

	return value;
}

Thank you

What are the requirements for "JsonDiscriminatorProperty" and "JsonDiscriminatorValue"?

I would like to serialize and deserialize multiple instances of a common interface. To do this, the README says I should use JsonDiscriminatorProperty and JsonDiscriminatorValue. However, it doesn't say what these actually do, or what the requirements are for using these decorators, so I will ask here:

Is it required that my superclass/interface has a property that describes the subclass type? For example, the Animal type in the example has a type:AnimalType field (pointed to by the JsonDiscriminatorProperty decorator). Is this required for (de)serialization to work?

In other words:

  1. Do I have to create a distinct value (e.g. via an enum) for all the subtypes of the interface/superclass I want to serialize?
  2. Do all instances need a property that is set to this value?

Cannot serialize property without type

When I try the quickstart using Typescript 2.6.1 I get this error:

    Cannot serialize property 'firstName' without type!

      at node_modules/ta-json/src/methods/serialize.ts:30:23
          at Map.forEach (<anonymous>)
      at node_modules/ta-json/src/methods/serialize.ts:28:22
          at Array.forEach (<anonymous>)
      at serializeRootObject (node_modules/ta-json/src/methods/serialize.ts:27:17)
      at Object.serialize (node_modules/ta-json/src/methods/serialize.ts:11:12)
      at Function.Object.<anonymous>.JSON.serialize (node_modules/ta-json/src/json.ts:15:16)
      at Function.Object.<anonymous>.JSON.stringify (node_modules/ta-json/src/json.ts:19:43)

I can make the error go away by adding @JsonElementType(String) below each of the @JsonProperty() decorators. I don't think this should be necessary though. Is this a bug or some bit of configuration I'm missing?

Constructor is not called

Hi, it seems like while deserializing, the constructor is never called. Is that the expected behavior? To me, that sounds very odd. The abstract class BaseDoc has a @JsonProperty declared, but the property is not set.

AccountDoc

import { BaseDoc } from "./base-doc";
import { JsonObject, JsonProperty } from "ta-json";

@JsonObject()
export class AccountDoc extends BaseDoc {

    @JsonProperty() fullName: string;
    @JsonProperty() email: string;
    @JsonProperty() password: string;

    constructor() {
        super("AccountDoc");
    }
}

BaseDoc

import { ObjectID } from "mongodb";
import { JsonProperty } from "ta-json";

export abstract class BaseDoc {

    @JsonProperty() private collectionName: string;
    
    _id: ObjectID;

    constructor(collectionName: string) {
        this.collectionName = collectionName;
    }

    public getId(): string { return this._id.toHexString(); }
    public get_id(): ObjectID { return this._id; }
    public getCollectionName(): string { return this.collectionName; }
    public setCollectionName(value: string) { this.collectionName = value; }
}

Test case

    it('should build AccountDoc class', () => {
        let body = { "fullName": "Mustafa Ekim", "email": "[email protected]", "password": "xxx" };
        let accountDoc: AccountDoc = TA.deserialize<AccountDoc>(body as any, AccountDoc);
        console.dir(accountDoc);
        expect(accountDoc.getCollectionName()).to.equal("AccountDoc");
        expect(accountDoc.fullName).to.be.equal("Mustafa Ekim");
        expect(accountDoc instanceof AccountDoc).to.equal(true);
    });

the collectionName property is never set.

AssertionError: expected undefined to equal 'AccountDoc'

Simple converters for serialize and deserialize

For simple converters, creating an IPropertyConverter seems a bit overkill. It would be nice to specify a function as part of a decorator for serialization or deserialization.

For example:

@JsonProperty('wifi_signal')
@JsonDeserailizer((value) => !value ? 0 : value * 20)
public wifiSignalStrength: number
@JsonProperty('wifi_signal')
@JsonSerializer((value) => !value ? 0 : value / 20)
public wifiSignalStrength: number

Thoughts?

Cheers,

  • Joe

is extending JSON a good idea?

Today I've checked many similar libraries but none of them extends JSON, instead, they offer some new static methods for transformation. Extending JSON does not sound like a good idea, why did you prefer such a way?

Typescript Class to Object

Hi, is it possible to transform an instance of a TypeScript class to an instance of Object type?

I know that I can achieve this by first stringifying and then parsing as Object however I wonder if there is a direct method for that.

(Why I need that? Because I want to persist my User into mongoDB with JsonDiscriminatorValues.)

Thanks!

Problem serializing arrays

Loving this library! Using it loads, just awesome.

I want to serialize an array as a single string:

@JsonProperty()
@JsonElementType(Joint)
public joints: Joint[];

I want a single string, not an array of strings.

I couldn't see a way of doing this, so I had to create my own class JointArray with push/splice methods and then implement a JointArrayConverter. Works but a bit long winded.

It would be nice to be able to specify a custom converter for an array, or even just a pre-serialization method on the parent class.

Cheers,
Paul

TypeScript Constructors are erased unless explicitly called - parsing fails

I have successfully used your framework to build a service but i am facing an issue. I have an entity class that I do not instanceiate any where in the code - it is only created by calls to TaJson.parse(). While can understand why tsc erases the constructor function - since it is not explicitly called - I wonder if there is a clever way to prevent it from being erased? The only ways i can come up with is to either create a dummy instance somewhere or to export a function that calls the constructor. Both feels a bit "hackish".

Ideas?

Optional vs. mandatory JSON properties during serialization and de-serialization

Go lang's JSON (un)marshal-er includes a handy omitempty "annotation" for struct fields:
https://golang.org/pkg/encoding/json/
"The 'omitempty' option specifies that the field should be omitted from the encoding if the field has an empty value, defined as false, 0, a nil pointer, a nil interface value, and any empty array, slice, map, or string."

There is a similar default behaviour in ta-json, during serialization to JSON:
https://github.com/edcarroll/ta-json/blob/master/lib/methods/serialize.ts#L33

if ((value === null || value === undefined) || p.writeonly) {
   return;
}

...as well as during deserialization from JSON:
https://github.com/edcarroll/ta-json/blob/master/lib/methods/deserialize.ts#L41

if ((value === null || value === undefined) || p.readonly) {
    return;
}

So, I have been wondering whether an additional @JsonRequired() decoration could be useful for class members (or conversely: @JsonOptional(), but this would require changing the current default behaviour). In case of an "empty" value (to be clarified, perhaps not exactly the same definition as Go lang), a property would not be outputted to JSON, and deserialization may "fail" if the property is actually required ("failure" may be an error callback where the API consumer could act accordingly).

...which also brings the question of TypeScript's strictNullChecks and the ? question mark to denote optionality:
https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html

Thoughts welcome :)

Use of interfaces

Does the library support using an interface type within a class? Say my class User has a property "animal" of type Animal, which is an interface? I didnt figure out how I can do that. It seems like I cannot use @jsontype(Animal)

deserialize does not return model

Hi, I don't know If I am fool but deserializing does not resolve an instance of my class. The test below fails

import { AccountDoc } from '../model/doc/account-doc';
import { expect } from 'chai';
import { JSON as TA } from "ta-json";

describe('Class Validation', () => {
    it('should build Account class', () => {
        let body = { fullName: "Mustafa Ekim", email: "[email protected]", password: "xxx" };
        let account = TA.deserialize<AccountDoc>(body, AccountDoc);
        expect(account.hi()).to.be.equal("That's Mustafa Ekim");
        expect(account instanceof AccountDoc).to.equal(true);
    });
});

TypeError: account.hi is not a function
AssertionError: expected false to equal true

Problem when using with Angular

Hi,

I was using the library on nodejs and everything was working as expected. Now I try to do the same thing on angular (with webpack) and somehow the library just breaks everything. I cannot see the real error but wherever I load the JSON (from "ta-json") Angular fails to load the whole file.

Uncaught Error: Unexpected value 'AppComponent' declared by the module 'AppModule'

I am getting the error above but I tried and tested many times that it only occurs if I use a simple JSON.deserialize or any other method it provides.

Any idea?

using in WebWorker

I use this library in angular's webworker, I have an error, because in webworker, window is not accessible :

Uncaught ReferenceError: Buffer is not defined
    at eval (converter.js:10)

Could you check nodeJS's environment otherwise ?

Maybe with this

var isNode=new Function("try {return this===global;}catch(e){return false;}");

Thank you

Map Support

I see you support Arrays and Sets, do you support Maps? I'm having some issues with them, and I don't see anything in the documentation.

Thanks.

serialize method don't change the properties

Hi!

I'm trying to serialize the following object:

@JsonObject()
export class Resale {
 
    @JsonProperty('codigo')
    code?: string;

}

Calling TaJson.serialize(resale) gives the following result:

{
    "code":"123"
}

How can I serialize the object to result in the following string?

 const resale: ResaleUpdate = {
        code: '123',
 };

Expected result:

{
    "codigo":"123"
}

I was thinking that serialization will use the value provided in the JsonProperty decorator. Is this right?

Thanks!

@JsonConstructor only invoked with JSON.deserialize(), not JSON.parse() (both use runConstructor: true)

To reproduce:

import {
    BeforeDeserialized, JsonConstructor, JsonElementType,
    JsonObject, JsonProperty, JSON, OnDeserialized,
} from "ta-json";

@JsonObject()
class Clazz {

    @JsonProperty()
    public foo: string;

    @JsonConstructor()
    private _JsonConstructor() {
        console.log("_JsonConstructor");
    }
}

console.log("===============");
console.log("===============");

let jsonObj = { foo: "bar" };
JSON.deserialize<Clazz>(jsonObj, Clazz, { runConstructor: true });

console.log("---------------");

let jsonString = '{"foo":"bar"}';
JSON.parse<Clazz>(jsonString, Clazz, { runConstructor: true });

console.log("===============");
console.log("===============");

Passing Object instead of String

I didnt observe any method that takes an object and cast to a typed TypeScript object. Should we always pass a string? It seems like I should wrap the js object with JSON.stringify(json) everytime.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.