Angular: How to map JSON result from HttpClient request to class instances? - json

I have the following code which seems wrong:
public search(searchString: string): Observable<Array<ClientSearchResult>> {
let params = new HttpParams().set('searchString', searchString);
return this.http
.get<Array<ClientSearchResult>>(this.searchUrl, { params: params })
.map((results: ClientSearchResult[]) => results.map((r: ClientSearchResult) => new ClientSearchResult(r)));
}
I know that the API is returning a JSON object which is not the same as an instance of my TypeScript class. However, I want to use properties defined in the TypeScript class.
Is there a better way to map the array coming from my API call to an array that actually consists of instances of ClientSearchResult?
Here is the ClientSearchResult object:
import { Name } from './name';
export class ClientSearchResult {
public id: string;
public name: Name;
public dateOfBirth: Date;
public socialSecurityNumber: string;
public get summary(): string {
let result: string = `${this.name}`;
if (this.dateOfBirth)
result += ` | ${this.dateOfBirth.toLocaleDateString()}`;
return result;
}
constructor(source: ClientSearchResult) {
this.id = source.id;
this.name = new Name(source.name);
this.dateOfBirth = source.dateOfBirth? new Date(source.dateOfBirth) : undefined;
this.socialSecurityNumber = source.socialSecurityNumber;
}
public toString(): string {
return this.summary;
}
}

We use a wonderful library to map json to typescript objects.
https://github.com/shakilsiraj/json-object-mapper
json-object-mapper depends on reflect-metadata library as it is using decorators to serialize and deserialize the data.

As an option you may try TypeScript as operator to cast your API response to the ClientSearchResult type.
import { Http, Response } from '#angular/http';
public search(searchString: string): Observable<ClientSearchResult[]> {
const params = new HttpParams().set('searchString', searchString);
return this.http.get(this.searchUrl, { params: params })
.map((results: Response) => results.json() as ClientSearchResult[]);
}
This approach requires your model class to be used as an interface, or just to be an interface:
interface ClientSearchResult {
id: number;
// etc
}

I have been using this very nice (and up-to-date at the time of posting) library:
https://www.npmjs.com/package/class-transformer
It can handle very complex cases with nested classes and more.

Related

value changed class variables to JSON string

I defined the following class:
'''
import { Deserializable } from '../deserializable';
export class Outdoor implements Deserializable {
ActualTemp: number;
TargetTemp: number;
Day: number;
deserialize(input: any): this {
// Assign input to our object
if(input){
Object.assign(this, input);
}
return this;
}
toJSON() {
return Object.assign({}, this);
}
}
'''
The toJSON function can generate a class data JSON String using
'''
const resource = JSON.stringify(this.appEngineMsg.Outdoor.toJSON());
'''
{"ActualTemp":60, "TargetTemp":55,"Day":23}
If I change the class object's variable this.appEngineMsg.Outdoor.TargertTemp, this.appEngineMsg.Outdoor.ActualTemp, then I would like to get the following JSON strings
{"Outdoor":{"TargetTemp":100}}
{"Outdoor":{"ActualTemp":100}}
What is the best way to do this?
Your question is not clear, do you want to print 2 differents JSON objects for each property?

HttpClient how to convert nested json and map to model domain

Hi I am pretty new to programing and i have been looking for an answer on the web for days for my problem without finding anything that works for me no matter what i do. I am receiving nest json as response. The json object amongst other things seem to have a dynamic key. What i want is to transform the json object to my model so that i can easily access the data in my template. Appreciate any help
here is an example of the json data
Amadeus API response
Here is my service
getResults(params: any) {
this.getItiniraries(params).subscribe((res) => {
this.itinirary = res;
// console.log(res);
this.updatedResults.next(this.itinirary);
});
}
getItiniraries(params: any): Observable<Itinirary> {
return this.http.get<Itinerary>('http://localhost:4202/api/itinirary' , {params: params})
; }
the models
Itinirary model
import { Result } from './result.model';
import { Meta } from '#angular/platform-browser';
// Model for data from Amadeus flight affiliate search
export class Itinirary {
public meta: Meta;
public results: Result[];
constructor(res: any) {
this.meta = res.meta;
this.results = res.results;
}
}
Result Model
import { Flight } from './flight.model';
import { Fare } from './fare.model';
export class Result {
public outbound_duration: string;
public outbound_flights: Flight[];
public inbound_duration: string;
public inbound_flights: Flight[];
public fare: Fare;
public cabin_code: string;
public fare_family: string;
public travel_class: string;
public merchant: string;
public airline: string;
public deep_link: string;
constructor(result: any) {
this.outbound_duration = result.outbound.duration;
this.outbound_flights = result.outbound.flights;
this.inbound_duration = result.inbound.duration;
this.inbound_flights = result.inbound.duration;
this.fare = result.fare;
this.cabin_code = result.cabin_code;
this.fare_family = result.fare_family;
this.travel_class = result.travel_class;
this.merchant = result.merchant;
this.airline = result.airline;
this.deep_link = result.deep_link;
}
}
Flight model
import { BookingInfo } from './bookingInfo.model';
export class Flight {
public departs_at: Date;
public arrives_at: Date;
public marketing_airline: string;
public operating_airline: string;
public flight_number: number;
public aircraft: number;
public booking_info: BookingInfo;
public origin_airport: string;
public origin_terminal: string;
public destination_airport: string;
public destination_terminal: string;
constructor(flight: any) {
this.departs_at = flight.departs_at;
this.arrives_at = flight.arrives_at;
this.marketing_airline = flight.marketing_airline;
this.operating_airline = flight.operating_airline;
this.flight_number = flight.flight_number;
this.aircraft = flight.aircraft;
this.booking_info = flight.booking_info;
this.origin_airport = flight.origin_airport;
this.origin_terminal = flight.origin_terminal;
this.destination_airport = flight.destination_airport;
this.destination_terminal = flight.destination_terminal;
}
}
Meta model
import { Carrier } from './carrier.model';
export class Meta {
public carriers: {[key: string]: Carrier };
constructor(meta: any) {
this.carriers = meta.carriers;
}
}
Carrier Model
export class Carrier {
public identifier: string;
public name: string;
public logoSmall: string;
public logoMedium: string;
constructor(carrier: any) {
this.identifier = carrier;
this.name = carrier.name;
this.logoSmall = carrier.logos.samll;
this.logoMedium = carrier.logos.medium;
}
}
in my flight model i would also like to add two properties date and time that derives from departure_at and arrival_at.
Basically i want to be able to pass the whole Intinirary object to my view so that i can through string iterpolation get the values.
ex after using ngFor or let result of itinirary.results
{{ result.outbound_flights[0].departure_date }} etc.
would really appreciate good guidance
updated service
getItiniraries(params: any): Observable<any> {
return this.http.get<any>('http://localhost:4202/api/itinirary' ,
{params: params})
.pipe(map((AmdResponse) => {
const parsedRes = JSON.parse(AmdResponse);
const itin = new Itinirary(parsedRes);
return itin;
} )); }
Updated meta Model and carrier Model
Meta
import { Carrier } from './carrier.model';
export class Meta {
public carriers: {[key: string]: Carrier };
constructor(meta) {
this.carriers = {};
Object.keys(meta.carriers).forEach(code => {
this.carriers[code] = new Carrier(meta.carriers[code]);
});
}
}
carrier model
export class Carrier {
public name: string;
public logoSmall: string;
public logoMedium: string;
constructor(cObject ) {
Object.keys(cObject).forEach(code => {
this.name = cObject.name;
});
Object.keys(cObject.logo).forEach(code => {
this.logoSmall = cObject.logos.samll;
this.logoMedium = cObject.logos.medium;
});
}
}
I also updated my Result model like this. Does it make sense?
import { Flight } from './flight.model';
import { Fare } from './fare.model';
export class Result {
public outbound_duration: string;
public outbound_flights: Flight[];
public inbound_duration: string;
public inbound_flights: Flight[];
public fare: Fare;
public cabin_code: string;
public fare_family: string;
public travel_class: string;
public merchant: string;
public airline: string;
public deep_link: string;
constructor(result) {
this.outbound_duration = result.outbound.duration;
// this.outbound_flights = this.loop(this.outbound_flights,
result.outbound.flights);
this.inbound_duration = result.inbound.duration;
// this.inbound_flights = this.loop(this.inbound_flights,
result.inbound.flights);
this.fare = new Fare(result.fare);
this.cabin_code = result.cabin_code;
this.fare_family = result.fare_family;
this.travel_class = result.travel_class;
this.merchant = result.merchant;
this.airline = result.airline;
this.deep_link = result.deep_link;
for (let i = 0; i < result.outbound.flights.length; i++) {
this.outbound_flights[i] = new Flight(result.outbound.flights[i]);
}
for (let i = 0; i < result.inbound.flights.length; i++) {
this.inbound_flights[i] = new Flight(result.inbound.flights[i]);
}
}
// loop(a, b) {
// for (let i = 0; i < b.length; i++) {
// a[i] = new Flight(b[i]);
// }
// return a;
// }
}
I tested both with a function or seperate loop.
I also added a dateFormatterService in my flight model, was not sure though where to import it to as i could not import it to constructor.
import { BookingInfo } from './bookingInfo.model';
import { DateFormatterService } from '../../Shared/dateFormatter.service';
export class Flight {
private df: DateFormatterService; // can i have it here instead of constructor?
public departs_date: string;
public departs_time: string;
public arrives_date: string;
public arrives_time: string;
public marketing_airline: string;
public operating_airline: string;
public flight_number: number;
public aircraft: number;
public booking_info: BookingInfo;
public origin_airport: string;
public origin_terminal: string;
public destination_airport: string;
public destination_terminal: string;
constructor(flight: any ) {
const depart_at = new Date(flight.departs_at);
const arrive_at = new Date(flight.arrives_at);
this.departs_date = this.df.transformDate(depart_at);
this.departs_time = this.df.transformTime(depart_at);
this.arrives_date = this.df.transformDate(arrive_at);
this.arrives_time = this.df.transformTime(arrive_at);
this.marketing_airline = flight.marketing_airline;
this.operating_airline = flight.operating_airline;
this.flight_number = flight.flight_number;
this.aircraft = flight.aircraft;
this.booking_info = new BookingInfo(flight.booking_info);
this.origin_airport = flight.origin_airport;
this.origin_terminal = flight.origin_terminal;
this.destination_airport = flight.destination_airport;
this.destination_terminal = flight.destination_terminal;
}}
As a preamble, understand there is no way to receive a JSON string (from an API or elsewhere) and make it become an instance of a specific custom Class.
I think you started with the right idea in your Meta constructor.... that you need to parse the received data into your own objects. You just need to go further, by explicitly parsing all properties of the received JSON into new objects (of your classes) one at a time until you're done. Tedious, maybe, but required if you want your own class hierarchy to represent the received data (which may NOT be needed, but that's not for me to decide).
I assume you've already got a JS literal object from the data received from the API response, something like this:
const itins = JSON.parse( amadeusResponse );
Then, you let your classes do the work:
const meta = new Meta( itins.meta );
//or
const alternate = new Meta();
alternate.fromJSON( itins.meta );
By the above, you can see you can either have a method that reads JSON data, or a constructor. The choice would be made by whether you would expect to ever create a Meta object without first having the JSON (in which case the method alternate is likely better).
In either case, the implementation reads the JS object you give it, and parses the received data structure into the structure you want to have in your local class instances. For example, in the Meta constructor...
constructor( meta ) {
this.carriers = {};
// which carriers did we get?
Object.keys(meta.carriers).forEach( code =>
this.carriers[code] = new Carrier( code, meta.carriers[code] )
);
In turn, the Carrier class constructor will read the "logos" and "name" properties, either into its fields, which may include even more class instances.
Keep going, until you're done.
If you want to create a tree of Javascript instances of models from JSON, you can map the JSON to the root class constructor like this:
getItiniraries(params: any): Observable<Itinerary> {
return this.http.get<Itinerary>('http://localhost:4202/api/itinirary', {params: params}).pipe(map(x => new Itinerary(x));
}
Then define the following constructor for all of your model classes:
constructor(obj) {
Object.assign(this, obj);
}
Then for all the children of a model class, replace these declarations public meta: Meta; with:
private _meta: Meta;
get meta() {
return this._meta;
}
set meta(value: any) {
this._meta = new Meta(value);
}
Same goes for children as Array:
private _results: Result[];
get results() {
return this._results;
}
set results(value: any) {
this._results = value.map(x => new Result(x));
}
Then your tree of objects will be composed of instances of your model classes, and you'll be able to benefit from the potential functions you'll define (for example data formatting functions, or whatever else)
using new es6 features it is quite easy this._results = value.map(x => new Result(x)); can bethis._results = [...value] this case if value is nested json array it will be flat as you want.

JSON to TypeScript in Angular

I have an http service in angular which makes call to web api to get JSON data which needs to be deserialized to typescript (.ts) objects. The name of key in JSON (i.e. "Property") is not matching to what I have in typescript (i.e. "property") - meaning JSON data is Uppercase and typescript is lowercase. Is there a mapping function with http calls in angular where I can deserialize the json to typescript?
Example JSON coming back from web api
"Packages": [
{
"IsESignAllowed": false,
"IsEnabled": true,
"Name": "Brokerage Accounts Package",
"PackageId": 2
}
]
Associated .ts file on Angular
export class Package {
public isESignAllowed: boolean;
public isEnabled: boolean;
public name: string;
public packageId: number;
}
You can add some serialisation functions to your Package class in typescript, this will allow you to map the JSON object from the server to a new Package object in TS.
As mentioned, it would be best to keep standard camel case convention everywhere - and have the server send JSON with lowercase property names, however, if this is not possible, this can still be handled in your serialisation functions.
export class Package {
public isESignAllowed: boolean;
public isEnabled: boolean;
public name: string;
public packageId: number;
static fromJson(jsonObj: any) {
const newPackage = new this();
newPackage.isESignAllowed = jsonObj.IsESignAllowed;
newPackage.isEnabled = jsonObj.IsEnabled;
newPackage.name = jsonObj.Name;
newPackage.packageId = jsonObj.PackageId;
return newPackage;
}
public toJson(): any {
const jsonObj: any = {};
jsonObj.IsESignAllowed = this.isESignAllowed;
jsonObj.IsEnabled = this.isEnabled;
jsonObj.Name = this.name;
jsonObj.PackageId = this.packageId;
return jsonObj;
}
}
Once you have received the JSON from the server, you can now map the array of packages over the new Package.fromJson() function, to create a list of Package objects.
const packages = serverResponse.Packages.map(packageJson => Package.fromJson(packageJson));

Moshi Custom JsonAdapter

I am trying to create a custom JsonAdapter for my JSON data that would bypass the serialization of specific field. Following is my sample JSON:
{
"playlistid": 1,
"playlistrows": [
{
"rowid": 1,
"data": {
"123": "title",
"124": "audio_link"
}
}
]
}
The JSON field data in above have dynamic key numbers, so I want to bypass this data field value and return JSONObject.
I am using RxAndroid, Retrofit2 with Observables. I have created a service class:
public static <S> S createPlaylistService(Class<S> serviceClass) {
Retrofit.Builder builder =
new Retrofit.Builder()
.baseUrl(baseURL)
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(httpClientBuilder.build())
.addConverterFactory(MoshiConverterFactory.create());
return builder.build().create(serviceClass);
}
I am calling this service using observable like this:
#GET("http://www.mylink.com/wp-json/subgroup/{subgroupId}/playlist/{comboItemId}")
Observable<Playlist> getPlaylist(#Path("subgroupId") int subgroupId, #Path("comboItemId") int comboItemId);
Then I run it like this:
ServiceBuilder.createPlaylistService(FHService.class).getPlaylist(123, 33);
My Pojo classes look like this:
public class Playlist {
#Json(name = "playlistid")
public Long playlistid;
#Json(name = "playlistrows")
public List<Playlistrow> playlistrows = null;
}
public class Playlistrow {
#Json(name = "rowid")
public Long rowid;
#Json(name = "data")
public Object data;
}
The problem is it would return a data value in this format:
{
123=title,
124=audio_link
}
which is invalid to parse as JSONObject.
I have Googled a lot and have also checked some Moshi example recipes but I had got no idea about how to bypass this specific field and return valid JSONObject, since I am new to this Moshi library.

Angular4 - why does custom toJSON() only get called on new objects?

I have this code. Notice that the serialization is simply renaming the template_items property to template_items_attributes:
export class Template {
constructor(
) {}
public id: string
public account_id: string
public name: string
public title: string
public info: string
public template_items: Array<TemplateItem>
toJSON(): ITemplateSerialized {
return {
id: this.id,
account_id: this.account_id,
name: this.name,
title: this.title,
info: this.info,
template_items_attributes: this.template_items
}
}
}
export interface ITemplateSerialized {
id: string,
account_id: string,
name: string,
title: string,
info: string,
template_items_attributes: Array<TemplateItem>
}
Creating an object locally works fine and stringify calls the toJSON() method.
However, once I send that object to the API:
private newTemplate(name: string): Template {
let template = new Template();
template.name = name;
template.account_id = this._userService.user.account_id;
// next 5 lines are for testing that toJSON() is called on new obj
let item = new TemplateItem();
item.content = "Test"
template.template_items.push(item);
let result = JSON.stringify(template);
console.log('ready', result); // SHOWS the property changes
return template;
}
postTemplate(name: string): Observable<any> {
return this._authService.post('templates', JSON.stringify(this.newTemplate(name)))
.map((response) => {
return response.json();
});
}
It is saved and returned, but from that point on when I stringify and save again it does NOT call toJSON().
patchTemplate(template: Template): Observable<any> {
console.log('patching', JSON.stringify(template)); // DOES NOT CHANGE!
return this._authService.patch('templates' + `/${template.id}`, JSON.stringify(template))
.map((response) => {
return response.json();
});
}
Why does toJSON() only work on new objects?
In fact, your question has nothing to do with Angular or Typescript, it's just some JavaScript and the logic of how serialization work and why do we serialize objects.
I send that object to the API, save and return it
When you return an "object" from an API, you're returning a string which you parse as a JSON serialized object. Then you get a plain JavaScript object, not an instance of your class.
Object prototype in JavaScript does not have toJSON method, and even if it had, it's not the method you've written inside the Template class, so it won't be called.
You don't even need a server call to replicate this, just do
const obj = JSON.parse(JSON.stringify(new Template()))
obj.toJSON // undefined
And you'll see that obj is not an instance of Template. It's simply an object which simply happens to have all the fields as your original object made as a Template instance, but it's not an instance of that class.