How do I cast a JSON object to a typescript class when properties doesn't match? - json

For example, I have a class:
export class SomeClass {
id: number;
name: string;
}
I receive JSON from server than looks like this
[{"Id":1,"Name":"typicalname"},{"Id":2,"Name":"somename"},{"Id":3,"Name":"blablabla"},{"Id":4,"Name":"lol"},{"Id":5,"Name":"lil"},{"Id":6,"Name":"lal"}]
How do I cast a JSON object to a typescript class when properties doesn't match?
That's how I do it wright now, and it's not working.
getSomeClass() {
return this.http.get(this.someClassUrl)
.map(response => <SomeClass[]>response.json())
.catch(this.handleError);
}

try this:
getSomeClass() {
return this.http.get(this.someClassUrl)
.map(response => {
let json = response.json();
return json.map(m => {
return {
id: json.Id,
name: json.Name
}
}
})
.catch(this.handleError);
}

When you have a type T and a value x and you write <T>x you are not performing a cast in a runtime sense. You are performing a type assertion. What this means is that you are telling TypeScript that the type of x is T.
In this particular case, if response.json() returns a value typed as any, which is not unreasonable for a deserialization operation, then <T>response.json() will be accepted by the TypeScript compiler for any T. This is because the type any is compatible with (technically assignable to) everything.
However in this case you want to verify the shape of the response and the compiler cannot do this for you. You need to write a validation algorithm that is appropriate.
What is appropriate will depend on the domain of your application, and may be non-trivial, but here is an example. Unfortunately, since your question implies Angular 2 and RxJS, even a simple applicable answer contains a fair amount of incidental complexity.
import {Http} from '#angular/http';
import {Observable} from 'rxjs/Observable';
import 'rxjs/add/operator/mergeMap';
import 'rxjs/add/operator/catch';
import 'rxjs/add/observable/of';
function injected(_) {} // emit decorator metadata with flag (not pertinent)
#injected export class SomeService {
constructor(readonly http: Http) {}
getSomeValue(): Observable<Expected> {
return this.http.get(this.someResourceUrl)
.catch(handleError)
.mergeMap(response => {
const deserialized = response.json();
if (isExpected(deserialized)) {
// note the type of derserialized is Expected in this block
return Observable.of(deserialized);
}
return Observable.throw('response data did not have the expected shape');
});
}
}
export interface Expected {
id: number;
name: string;
}
function isExpected(deserialized : any): deserialized is Expected {
return typeof deserialized.id === 'number' && typeof deserialized.name === 'string';
}
function handleError(error) { // this is not really necessary, but was in the question
console.error(error); // log
return Observable.throw(error); // rethrow.
}
The most significant thing here is the isExpected function.
It takes a value of any type, validates it based on our criteria, and states that if it returns true, then the given value was indeed of the expected type, Expected.
What does it mean to be of the expected type?
Well our isExpected function determines that, and provides this information to the TypeScript language by way of its return type which says that if the function returns true, then the value passed to it is of type Expected.
This is known as a User Defined Type Guard function and you can read more about it at https://www.typescriptlang.org/docs/handbook/advanced-types.html.

Related

Type safe deserialization in TypeScript

I have this sample TypeScript code that is supposed to deserialize a simple JSON into an instance of class Person and then call foo method on it, but it doesn't work:
class Person {
name!: string;
age!: number;
foo() {
console.log("Hey!");
}
}
fetch("/api/data")
.then(response => {
return response.json() as Promise<Person>;
}).then((data) => {
console.log(data);
data.foo();
});
The output of console show that object is in a proper shape, but it is not recognized as Person:
Object { name: "Peter", age: 44 }
​
age: 44
​name: "Peter"
​
Thus when it tries to call foo method it fails:
Uncaught (in promise) TypeError: data.foo is not a function
http://127.0.0.1:8000/app.js:14
promise callback* http://127.0.0.1:8000/app.js:12
How can I fix it? Should I use Object.assign or there is another better/native solution?
let x = (<any>Object).assign(Object.create(Person.prototype), data);
x.foo();
Remember, TypeScript is just a way of annotating JavaScript code with type guards. It doesn't do anything extra. For example, saying that the object returned by response.json() should be treated as a Promise<Person> does not mean it will invoke the constructor of your Person class. Rather, you'll just be left with a plain old JavaScript object that has a name and an age.
It looks to me like you'll need to create a constructor for your Person class which can create a new instance of a Person based on an object that matches its interface. Something like this, perhaps?
interface PersonLike {
name: string;
age: string;
}
class Person implements PersonLike {
constructor(data: PersonLike) {
this.name = data.name;
this.age = data.age;
}
name: string;
age: string;
foo() {
console.log("Hey!");
}
}
fetch("/api/data")
.then(response => {
return response.json() as Promise<PersonLike>;
}).then((data) => {
const person = new Person(data);
person.foo();
});
I'd also recommend using a type guard instead of the as keyword, in case the API you're fetching data from changes. Something like this, perhaps:
function isPersonLike(data: any): data is PersonLike {
return typeof data?.name === 'string' && data?.age === 'string';
}
fetch("/api/data")
.then(response => {
return response.json();
}).then((data: unknown) => {
if (isPersonLike(data)) {
const person = new Person(data);
person.foo();
}
});
... is supposed to deserialize a simple JSON into an instance of class Person and then ...
Unfortunately, generic type in TypeScript only works as some kind of model design assistant. It will never be compiled into JavaScript file. Take your "fetch" code for example:
fetch("/api/data")
.then(response => {
return response.json() as Promise<Person>;
}).then((data) => {
console.log(data);
data.foo();
});
After compile the above TypeScript file into JavaScript, we can find the code as Promise<Person> is completely removed:
fetch("/api/data")
.then(function (response) {
return response.json();
}).then(function (data) {
console.log(data);
data.foo();
});
To implement "type safe deserialization", you need to save class/prototype information during serialization. Otherwise, these class/prototype information will be lost.
... or there is another better/native solution? ... BTW, what if a class field has a custom type, so it is an instance of another class?
No, there is no native solution, but you can implement "type safe" serialization/deserialization with some libraries.
I've made an npm module named esserializer to solve this problem automatically: save JavaScript class instance values during serialization, in plain JSON format, together with its class name information. Later on, during the deserialization stage (possibly in another process or on another machine), esserializer can recursively deserialize object instance, with all Class/Property/Method information retained, using the same class definition. For your "fetch" code case, it would look like:
// Node.js server side, serialization happens here.
const ESSerializer = require('esserializer');
router.get('/api/data', (req, res) => {
// ...
res.json(ESSerializer.serialize(anInstanceOfPerson));
});
// Client side, deserialization happens here.
const ESSerializer = require('esserializer');
fetch("/api/data")
.then(response => {
return response.text() as Promise<string>;
}).then((data) => {
const person = ESSerializer.deserialize(data, [Person, CustomType1, CustomType2]);
console.log(person);
person.foo();
});

In Angular 2, how do you intercept and parse Infinity / NaN bare-words in JSON responses?

I am writing an Angular front end for an API that occasionally serves Infinity and -Infinity (as bare words) in the JSON response object. This is of course not compliant with the spec, but is handled a few different JSON libraries, albeit optionally. I have an Angular service in place that can successfully retrieve and handle any retrieved entity that does not have these non-conforming values. Additionally, I have managed to get an HttpInterceptor in place which just logs when events trickle through, just to be sure I have it connected properly.
The issue that I am facing is that the HttpInterceptor seems to allow me to do one of two things:
Catch/mutate the request before it is sent to the API, or
Catch/mutate the request after it comes back from the API, and also after it is parsed.
What I would like to do is very similar to this question for native javascript, but I have not been able to determine if it is possible to tie into the replacer function of JSON.parse in the Angular Observable pipe (I think that if tying into that is possible it would solve my issue).
I have also found this question for Angular which is close, but they appear to have been able to handle changing the response to something other than the bare-words, which I don't have the liberty of doing.
This is the current implementation of my HttpInterceptor, note that it does not actually make any changes to the body. When retrieving an entity without these bare-word values, it logs to the console and all is well. When retrieving an entity with any of these bare-word values, an error is thrown before the HERE line is hit.
function replaceInfinity(body: string): string {
// Do something
return body;
}
#Injectable()
export class JsonInfinityTranslator implements HttpInterceptor {
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
return next.handle(req).pipe(
map((event) => {
if (event instanceof HttpResponse) {
console.log("HERE");
return event.clone({body: replaceInfinity(event.body)});
} else {
return event;
}
})
);
}
}
TL;DR: Is there a way to mutate the body text of the returned response before the Angular built in JSON deserialization?
I was able to figure out how to achieve this, and it came down to:
Modifying the request to return as text instead of json
Catch the text response and replace the bare word symbols with specific string flags
Parse the text into an object using JSON.parse, providing a reviver function to replace the specific string flags with the javascript version of +/-Infinity and NaN
Here's the Angular HttpInterceptor I came up with:
import {Injectable} from '#angular/core';
import {HttpEvent, HttpHandler, HttpInterceptor, HttpRequest, HttpResponse} from '#angular/common/http';
import {Observable} from 'rxjs';
import {map} from 'rxjs/operators';
#Injectable()
export class JsonBareWordNumericSymbolTranslator implements HttpInterceptor {
private static infinityFlag = '__INFINITY_FLAG__';
private static negInfinityFlag = '__NEG_INFINITY_FLAG__';
private static nanFlag = '__NAN_FLAG__';
private static replaceBareWordSymbolsWithFlags(body: string): string {
const infinityBareWordPattern = /(": )Infinity(,?)/;
const negInfinityBareWordPattern = /(": )-Infinity(,?)/;
const nanBareWordPattern = /(": )NaN(,?)/;
return body
.replace(infinityBareWordPattern, `$1"${this.infinityFlag}"$2`)
.replace(negInfinityBareWordPattern, `$1"${this.negInfinityFlag}"$2`)
.replace(nanBareWordPattern, `$1"${this.nanFlag}"$2`);
}
private static translateJsonWithFlags(substitutedBody: string): any {
return JSON.parse(substitutedBody, (key: string, value: string) => {
if (value === this.infinityFlag) {
return Infinity;
} else if (value === this.negInfinityFlag) {
return -Infinity;
} else if (value === this.nanFlag) {
return NaN;
} else {
return value;
}
});
}
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
if (req.responseType !== 'json') {
// Do not modify requests with response types other than json
return next.handle(req);
}
return next.handle(req.clone({responseType: 'text'})).pipe(
map((event) => {
if (!(event instanceof HttpResponse)) {
return event;
}
const substitutedBody = JsonBareWordNumericSymbolTranslator.replaceBareWordSymbolsWithFlags(event.body);
const parsedJson = JsonBareWordNumericSymbolTranslator.translateJsonWithFlags(substitutedBody);
return event.clone({body: parsedJson});
})
);
}
}

Should I manually parse a JSON string received from a server while the type of the parameter which represents the object being received is declared?

I am handling Http result in a reducer function of an Observable. While a type of the parameter jwt is set as { id: string, auth_token: string, expires_in }, the jwt argument turns out to be a string. I thought TypeScript does parsing automatically. Do I have to do JSON.parse(JSON.stringify(jwt)) by myself?
.mergeMap((jwt: { id: string, auth_token: string, expires_in }) => {})
Type checking external code
There is no relationship between the TypeScript source code and the JavaScript outputs that gets executed at run-time. TypeScript is only effective at catching compile-time errors if the compiled types match the run-time types.
Normally, this isn't a problem. But in scenarios where you call out to external code (i.e. AJAX call to fetch data from the server), there is no guarantee that the response will be the type you expect. So you must be cautious in these scenarios.
Your specific example
I suspect that your code has a variable jwt with type any and you just assigned the type to { id: string, auth_token: string, expires_in } when in fact, jwt was of type string as far as javascript is concerned.
In this case, you already found your solution, JSON.parse(str). This converts the json string into a javascript object.
Now that you have an object, you can use duck typing to infer the run-time type and let typescript know about the type at compile-time via type guards.
Solution
function isDate(obj: any): obj is Date {
return typeof obj === 'object' && 'toISOString' in obj;
}
function isString(obj: any): obj is string {
return typeof obj === 'string';
}
interface JWT {
id: string;
auth_token: string;
expires_in: Date;
}
function isJwt(obj: any): obj is JWT {
const o = obj as JWT;
return o !== null
&& typeof o === 'object'
&& isString(o.id)
&& isString(o.auth_token)
&& isDate(o.expires_in);
}
function print(jwt: any) {
if (typeof jwt === 'string') {
try {
jwt = JSON.parse(jwt);
} catch (e) {
console.error(`String is not JSON: ${jwt}`);
}
}
if (isJwt(jwt)) {
console.log(`Found jwt: ${jwt.id} ${jwt.auth_token} ${jwt.expires_in}`);
} else {
console.error(`Object is not of type jwt: ${jwt}`);
}
}
print(42);
print('failing');
print(null);
print(undefined);
print({});
print({ id: 'id01', auth_token: 'token01', expires_in: new Date(2018, 11, 25) });
Playground
Try running that code on the TS Playground to see how it inspects the object at run-time.
If the jwt(JSON) object is retrieved with Http from HttpModule #angular/http you have to parse it to JSON
e.g.:
import { Http } from '#angular/http';
....
constructor(
private http: Http
...
) {}
this.http.get(url)
.map((res: any) => {
return res.json();
}).subscribe( (jwt: any) => {
//you got jwt in JSON format
});
If you use HttpClient from HttpClientModule #angular/common/http (Angular > 4.3.x) you do not need to parse the received data because it is already done.
import { HttpClient } from '#angular/common/http';
....
constructor(
private http: HttpClient
...
) {}
this.http.get<any>(url)
.subscribe((jwt: any) => {
//you got jwt in JSON format
})
More info in this answer

Passing generics into Promise like Promise<T>?

I have the following function:
class RestService {
public async get<T>(func: string): Promise<T> {
var toRet = {};
await fetch(EndPoint + func)
.then(response => response.json() as Promise<T>)
.then(data => {
toRet = data;
})
.catch(e => {
});
return toRet as T;
}
}
Everything works fine but the response I get in 'data' is ALWAYS a generic object.
For example I might have a model like so:
class Model
{
string name;
}
and call the function like so:
get<Model>("getmodel")
The response is ALWAYS a generic object that looks like:
{name:"some name"}
From my understanding generics are supported in Typescript and Promise takes in variable types, my only thought is that I can't pass a generic into a generic?
Maybe a better way to write it would be this way.
class RestService {
public async get<T>(func: string): Promise<T | void> {
return await fetch('' + func)
.then(response => response.json() as Promise<T>)
.then(data => {
return data;
})
.catch(e => {
});
}
}
You can see it in the playground too at this link.
This way you don't have to overwrite any types and the compiler can figure out everything on it's own.
The return type is now Promise<T | void> because the catch function doesn't return anything. You could have something else or nothing depending what you do in case of an error.
Typescript will not transform the data object to match the T type you give in automatically.
If for example you call the method with get<AnotherModel>('modelEndpoint') but the endpoint returns Model. While the type at build time will say you should expect an object of type AnotherModel at runtime the object will in fact be of type Model.
This isn't clear from the question but maybe your issue is with the fact that the data is of type T instead of the Promise<T> that you return in the previous then callback.
If that is the case, that's because any Promise sent as a callback to the then function is resolved first before the outer then is called.
That means your code is equivalent to.
.then(response => response.json().then((data) => data as T))
.then(data => {
return data;
})
It's just that the Promise api will just take care of that for you.
If you want to learn more about the pitfalls of Promises in Javascript this post is quite good.

Deserializing json in Angular 2/4 using HttpClientModule

So I'm getting the following JSON structure from my asp.net core api:
{
"contentType": null,
"serializerSettings": null,
"statusCode": null,
"value": {
"productName": "Test",
"shortDescription": "Test 123",
"imageUri": "https://bla.com/bla",
"productCode": null,
"continuationToken": null
}
}
I have the following typescript function that invokes the API to get the above response:
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; //not working...
console.log("DATA: " + data);
console.log("DATA: " + data['value']);
return data;
});
}
ProductVM:
export interface ProductVM {
productName: string;
shortDescription: string;
imageUri: string;
productCode: string;
continuationToken: string;
}
My problem is that I can't deserialize it to ProductVM. The console logs just produce [object Object]
How can I actually map the contents of the value in my json response to a ProductVM object?
Is it wrong to say that data is a ProductVM in the map function? I have tried lots of different combinations but I cannot get it to work!
I'm unsure whether I can somehow automatically tell angular to map the value array in the json response to a ProductVM object or if I should provide a constructor to the ProductVM class (it's an interface right now), and extract the specific values in the json manually?
The data object in the map method chained to http is considered a Object typed object. This type does not have the value member that you need to access and therefore, the type checker is not happy with it.
Objects that are typed (that are not any) can only be assigned to untyped objects or objects of the exact same type. Here, your data is of type Object and cannot be assigned to another object of type ProductVM.
One solution to bypass type checking is to cast your data object to a any untyped object. This will allow access to any method or member just like plain old Javascript.
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map((data: any) => this.externalProduct = data.value);
}
Another solution is to change your API so that data can deliver its content with data.json(). That way, you won't have to bypass type checking since the json() method returns an untyped value.
Be carefull though as your any object wil not have methods of the ProductVM if you ever add them in the future. You will need to manually create an instance with new ProductVM() and Object.assign on it to gain access to the methods.
From angular documentation: Typechecking http response
You have to set the type of returned data when using new httpClient ( since angular 4.3 ) => this.http.get<ProductVM>(...
public externalProduct: ProductVM;
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get<ProductVM>("api/product?productCode=" + code)
.map((data: ProductVM) => {
this.externalProduct = data; // should be allowed by typescript now
return data;
});
}
thus typescript should leave you in peace
Have you tried to replace
this.externalProduct = data;
with
this.externalProduct = data.json();
Hope it helps
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get("api/product?productCode=" + code)
.map(data => {
this.externalProduct = <ProductVM>data;
console.log("DATA: " + this.externalProduct);
return data;
});
}
So, first we convert the response into a JSON.
I store it into response just to make it cleaner. Then, we have to navigate to value, because in your data value is the object that corresponds to ProductVM.
I would do it like this though:
Service
getProductExternal(code: string): Observable<ProductVM> {
return this.http.get(`api/product?productCode=${code}`)
.map(data => <ProductVM>data)
.catch((error: any) => Observable.throw(error.json().error || 'Server error'));
}
Component
this.subscription = this.myService.getProductExternal(code).subscribe(
product => this.externalProduct = product,
error => console.warn(error)
);
I used this approach in a client which uses the method
HttpClient.get<GENERIC>(...).
Now it is working. Anyway, I do not understand, why I do not receive a type of T back from the http client, if I don't use the solution provided in the answer above.
Here is the client:
// get
get<T>(url: string, params?: [{key: string, value: string}]): Observable<T> {
var requestParams = new HttpParams()
if (params != undefined) {
for (var kvp of params) {
params.push(kvp);
}
}
return this.httpClient.get<T>(url, {
observe: 'body',
headers: this.authHeaders,
params: requestParams
}).pipe(
map(
res => <T>res
)
);
}