Skip to content

Commit

Permalink
Merge pull request #2 from edcarroll/develop
Browse files Browse the repository at this point in the history
v1.1.0 into Master
  • Loading branch information
edcarroll authored Jan 2, 2017
2 parents 0a947fa + 8dbb57b commit d4558ff
Show file tree
Hide file tree
Showing 17 changed files with 262 additions and 70 deletions.
79 changes: 76 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Type-Aware JSON Parser & Serializer (ta-json)

Strongly typed JSON parser & serializer for TypeScript / ES7 via decorators. Supports parameterized class constructors, nesting classes, `Array`s and `Set`s, custom property converters and more.
Strongly typed JSON parser & serializer for TypeScript / ES7 via decorators.

Supports [parameterized class constructors](#jsonobject), nesting classes, [inheritance](#jsondiscrimatorpropertypropertystring--jsondiscriminatorvaluevalueany), [`Array`s and `Set`s](#jsonelementtypetypefunction), [custom property converters](#jsonconverterconverteripropertyconverter--parameterlessconstructor) and more.

## Installation

Expand Down Expand Up @@ -47,6 +49,8 @@ person instanceof Person; // true
person.fullName; // Edward Carroll
```

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

## Decorators

### @JsonObject()
Expand Down Expand Up @@ -117,7 +121,7 @@ export class LotteryDraw {
}
```

### @JsonConstructor
### @JsonConstructor()

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.

Expand All @@ -140,10 +144,56 @@ export class Demo {
}
```

### @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 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

```typescript
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 } ]
```

### @JsonConverter(converter:IPropertyConverter | ParameterlessConstructor<IPropertyConverter>)

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.
Expand Down Expand Up @@ -200,7 +250,7 @@ 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
### @JsonReadonly()

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

Expand All @@ -219,6 +269,29 @@ export class Person {
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

```typescript
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
Expand Down
36 changes: 35 additions & 1 deletion index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,38 @@
export * from "./lib/json";
export * from "./lib/types";
export * from "./lib/decorators";
export * from "./lib/converters";
export * from "./lib/converters";

import {JSON} from "./lib/json";
import {JsonObject, JsonDiscriminatorProperty, JsonProperty, JsonDiscriminatorValue} from "./lib/decorators";

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()];

console.log(JSON.parse<Animal[]>('[{"type":0},{"type":1}]', Animal)); //
25 changes: 25 additions & 0 deletions lib/classes/object-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import {PropertyDefinition} from './property-definition';

export class ObjectDefinition {
public ctr:() => void;
public discriminatorField:string;
public discriminatorValue:any;
public properties:Map<string, PropertyDefinition> = new Map<string, PropertyDefinition>();

public getProperty(key:string) {
Expand All @@ -25,4 +27,27 @@ export function getDefinition(target:Function) {
objectDefinitions.set(target, definition);
}
return definition;
}

export function getInheritanceChain(type:Object):Function[] {
if (!type) {
return [];
}
const parent = Object.getPrototypeOf(type);
return [type.constructor].concat(getInheritanceChain(parent))
}

export function getChildClassDefinitions(parentType:Function) {
const parentDef = getDefinition(parentType);
const childDefs:[Function, ObjectDefinition][] = [];

if (parentDef.discriminatorField) {
objectDefinitions.forEach((def, type) => {
const superClass = Object.getPrototypeOf(type.prototype).constructor;
if (superClass == parentType) {
childDefs.push([type, def]);
}
});
}
return childDefs;
}
1 change: 1 addition & 0 deletions lib/classes/property-definition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export class PropertyDefinition {
array:boolean = false;
set:boolean = false;
readonly:boolean = false;
writeonly:boolean = false;
converter:IPropertyConverter;
serializedName:string;

Expand Down
5 changes: 4 additions & 1 deletion lib/decorators/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,7 @@ export * from "./json-type";
export * from "./json-element-type";
export * from "./json-converter";
export * from "./json-readonly";
export * from "./json-constructor";
export * from "./json-writeonly";
export * from "./json-constructor";
export * from "./json-discriminator-property";
export * from "./json-discriminator-value";
2 changes: 1 addition & 1 deletion lib/decorators/json-constructor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {getDefinition} from '../classes/object-definition';

export function JsonConstructor() {
return function(target:any, key:string):void {
let definition = getDefinition(target.constructor);
const definition = getDefinition(target.constructor);

definition.ctr = target[key];
};
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/json-converter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import {ParameterlessConstructor} from '../types';

export function JsonConverter(converter:IPropertyConverter | ParameterlessConstructor<IPropertyConverter>) {
return function(target:any, key:string):void {
let property = getDefinition(target.constructor).getProperty(key);
const property = getDefinition(target.constructor).getProperty(key);

if (typeof converter === "function") {
property.converter = new converter();
Expand Down
7 changes: 7 additions & 0 deletions lib/decorators/json-discriminator-property.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {getDefinition} from '../classes/object-definition';

export function JsonDiscriminatorProperty(property:string) {
return function(constructor:Function):void {
getDefinition(constructor).discriminatorProperty = property;
};
}
7 changes: 7 additions & 0 deletions lib/decorators/json-discriminator-value.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import {getDefinition} from '../classes/object-definition';

export function JsonDiscriminatorValue(value:any) {
return function(constructor:Function):void {
getDefinition(constructor).discriminatorValue = value;
};
}
2 changes: 1 addition & 1 deletion lib/decorators/json-element-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {getDefinition} from '../classes/object-definition';

export function JsonElementType(type:Function) {
return function(target:any, key:string):void {
let property = getDefinition(target.constructor).getProperty(key);
const property = getDefinition(target.constructor).getProperty(key);

property.type = type;
};
Expand Down
4 changes: 2 additions & 2 deletions lib/decorators/json-property.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import {getDefinition} from '../classes/object-definition';

export function JsonProperty(propertyName?:string) {
return function(target:any, key:string):void {
let type = Reflect.getMetadata("design:type", target, key);
const type = Reflect.getMetadata("design:type", target, key);

let property = getDefinition(target.constructor).getProperty(key);
const property = getDefinition(target.constructor).getProperty(key);
property.serializedName = propertyName || key;
property.array = type === Array;
property.set = type === Set;
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/json-readonly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {getDefinition} from '../classes/object-definition';

export function JsonReadonly() {
return function(target:any, key:string):void {
let property = getDefinition(target.constructor).getProperty(key);
const property = getDefinition(target.constructor).getProperty(key);

property.readonly = true;
};
Expand Down
2 changes: 1 addition & 1 deletion lib/decorators/json-type.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import {getDefinition} from '../classes/object-definition';

export function JsonType(type:Function) {
return function(target:any, key:string):void {
let property = getDefinition(target.constructor).getProperty(key);
const property = getDefinition(target.constructor).getProperty(key);

property.type = type;
};
Expand Down
9 changes: 9 additions & 0 deletions lib/decorators/json-writeonly.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import {getDefinition} from '../classes/object-definition';

export function JsonWriteonly() {
return function(target:any, key:string):void {
const property = getDefinition(target.constructor).getProperty(key);

property.writeonly = true;
};
}
Loading

0 comments on commit d4558ff

Please sign in to comment.