Angular Http Deserializer
Want to get started? Head straight to Usage.
The current Angular Http Client, since it uses typescript, uses duck typing for returning objects. i.e. The object being returned is not an instance of the model, it's a json object emulating the shape of the model object.
Therefore, any getters, methods, etc. on the model object being returned will not exist and instanceof will not work. This library was built specifically in order to deserialize deep object models and provide a way of easily returning constructed instances of the model objects from the Angular Http Client.
Without the use of this module, the objects being created by the angular http client are not instances of the model.
showConfig() {
this.configService.getConfig()
.subscribe((data: Config) => {
if (!(data instanceof Config)) {
throw new Error('data is not instance of Config.');
}
});
}
A dialog of the issue can be seen here.
Some solutions on SO include:
This solution is not simple enough and requires you to write constructors for each object in order for your object to be deserialized.
Deeply nested (including array) objects are not handled with this solution without more custom constructor code.
This solution has a number of pitfalls. The Date data type is entirely missed because the incoming Json type will be string or number and will be assigned as such on the constructed object, thereby overwriting the property with the wrong type, which fail silently because of typescript's duck typing.
Fail:
class Cow {
sound: string;
createdDate: Date;
}
let cow: Cow = Object.assign(new Cow(), {
createdDate: '1/1/2018 12:00pm'
});
// Fails
expect(cow.createdDate instanceOf Date).toBeTruth();
This has the previously mentioned issues, along with being very poor performance as described here.
All of previously mentioned issues.
This solution you have to write static createInstance, a constructor code and it has the aforementioned pitfalls.
The following are the required prerequisites:
- Use TypeScript
- Installation
- Configuration Completed
- Model Annotation
- Converters
- Http Client Deserializer Injection
- Test Updates
- Expected Exceptions
- Example Angular Project
npm install angular-http-deserializer --save
In order to utilize and emit the necessary @dataType annotation you have to setup typescript within tsconfig.json.
{
...
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
}
This library uses a minimalistic model annotation methodology in order to reduce the amount of effort involved in annotating the model object. This was one of the downsides of existing deserialization libraries on npm.
You only have to provide a @dataType annotation on the types Date, Object or properties that require converters.
import { dataType, skip, converters } from 'angular-http-deserializer/decorators';
export class User {
id: number;
@skip()
name: string;
@dataType(Date)
@converters({
'number': (input: number) => {
let convertedDate = new Date(input);
// Used for validation of the converter.
convertedDate.setMilliseconds(123);
return convertedDate;
}
})
createdDate: Date;
get wasCreatedFirstOfMonth() : boolean {
return this.createdDate.getDate() == 1;
}
}
export class Product {
id: number;
name: string;
get hasName(): boolean {
return this.name && this.name.length > 0;
}
}
export class OrderProduct {
@dataType(Product)
product: Product;
quantity: number;
}
export class Order {
id: number;
@dataType(OrderProduct, true) // Second parameter indicates isArray
products: OrderProduct[];
@dataType(User)
orderedBy: User;
@dataType(Date)
createdDate: Date;
}
#Converters
Converters are created based on an object with a key of the expected incoming type and returning the expected output type for that property. All expected input types must have converters or an exception is thrown.
export class ConverterOrder {
id: number;
@dataType(Complex.OrderProduct, true)
products: Complex.OrderProduct[];
@dataType(Complex.User)
orderedBy: Complex.User;
@dataType(Date)
@converters({
'number': (input: number) => {
let convertedDate = new Date(input);
// Used for validation of the converter.
convertedDate.setMilliseconds(suffixedMs);
return convertedDate;
}
})
createdDate: Date;
}
The above example shows a converter defined for the createdDate property with an incoming data type of number, which creates a Date type.
If you do not cover all of the potential incoming data types into the converter, an Error will be thrown.
You'll need to first import the deserializer into your service.
import deserializer from 'angular-http-deserializer';
NOTE: Take note of the ending r. Deserializer creates a deserialization function for you (productino a deserialize function). Deserialize does the deserialization itself.
From the angular http client examples:
showConfig() {
this.configService.getConfig()
// Deserializer function provided to map function.
.map(deserializer<Config>(Config))
.subscribe((data: Config) => this.config = {
heroesUrl: data['heroesUrl'],
textfile: data['textfile']
});
}
Without the mapping and proper deserialization, the objects coming out of the Http Client will fail instanceof checks.
Since your view can now expect non-duck typed objects (real) you'll likely want to change around some test code. You'll want to take your regular json objects within your tests and convert them into real objects. There's two ways you can do that, you can construct the normal deserializer as you would within a service that uses the Http Client or you can use the deepDeserializer. This is an example of how this would work.
import { deserialize } from 'angular-http-deserializer';
let productQtyJson {
product: null,
quantity: 14112
};
let productQty: ProductQuantity = deserialize<ProductQuantity>(ProductQuantity, productQtyJson);
This way, within your tests the proper objects will now be available and property getters and methods will be accessible.
The deserializer is built fairly resiliently so that most things pass. Currently, no custom deserialization is built into the annotation to enable overriding how deserialization works. Ya get what ya get.
There are 6 expected exceptions within this module. They are the following:
Reason: When a property is an object, but offers no @dataType annotation to deserialize object. Message: DataType annotation missing on Type ${type.prototype.constructor.name} field ${key}
Reason: When a data annotation is marked as array, but the data is not an array. Messages: Array deserialization error. ${type.prototype.constructor.name}.${key} must be array. Array deserialization error. Object must be array.
Reason: When a data annotation is not marked as array, but the data is. Message: ${type.prototype.constructor.name}.${key} array not expected.
Reason: Dates may be cast from 2 types, string and number. Null or undefined are simply returned. If an unexpected data type is found, an exception is thrown. Message: Date cannot be cast from type ${expectedType}
Reason: When a converter has been supplied, but not all of the necessary converters. When one is supplied, you have to cover all of the necessary cases for conversion. Mesage: Converters for property type ${propTypeString} required.
Reason: Message: Converters cannot be skipped for property ${propertyName}.
Here is a sample project commit if you'd like to view the changes necessary to implement the angular-http-deserializer.
You'll want to review the updates to the method signature commit as well.