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.
Related
Friends
I have a simple Dart class that cannot be encoded into JSON.
The output of the following code prints out to the console
flutter: Converting object to an encodable object failed: Instance of 'TestJsonConversion'
class TestJsonConversion {
String testString = "123245abcde";
int testIneger = 1234;
}
void main() {
var testJsonConversion = TestJsonConversion();
try {
var testString = jsonEncode(testJsonConversion);
// ignore: avoid_print
print(testString);
}catch(e){
// ignore: avoid_print
print(e.toString());
}
runApp(const MyApp());
}
This is the default application generated by Visual Studio with just these lines added.
You cannot encode an instance of a user class with the built-in jsonEncode. These are things you can encode by default: "a number, boolean, string, null, list or a map with string keys". For this class to encode, you'd have to define a .toJson method on it, and I don't see one there.
The class has no constructors and tojson . Try this
class TestJsonConversion {
final String testString;
final int testInteger;
TestJsonConversion(this.testString, this.testInteger);
TestJsonConversion.fromJson(Map<String, dynamic> json)
: testString = json['trstString'],
testInteger = json['testInteger'];
Map<String, dynamic> toJson() => {
'testString': testString,
'testInteger': testInteger,
};
}
And when you create an instance
var testJsonConversion = TestJsonConversion(testString: 'abc', testInteger: 123);
print(json.encode(testJsonConversion.toJson());
I followed code from this example but my toJSON() function is not called.
Attempt 1
export class Template {
constructor(
) {}
public id: string
public account_id: string
public name: string
public title: string
public info: string
public template_items: Array<number>
public toJSON = function() {
return {
attributes: this.template_items
}
}
}
Attempt 2
interface ITemplateSerialized {
attributes: Array<number>
}
export class Template {
constructor(
) {}
public id: string
public account_id: string
public name: string
public title: string
public info: string
public template_items: Array<number>
toJSON(): ITemplateSerialized {
return {
attributes: this.template_items
}
}
}
Attempt 3
Identical code to Attempt 2 except the toJSON is:
public toJSON = function(): ITemplateSerialized {
return {
attributes: this.template_items
}
}
Create some data...eg:
let t = new Template();
t.name = "Mickey Mouse"
t.template_items = [1,2,3]
console.log(JSON.stringify(t));
In all cases it does not change template_items to attributes...what am I missing here?
UPDATE
The provided plunk by #estus in the comments worked, so I decided to make one in Angular, to compare. Here it is and it works.
When I wrote the question, to make the code simple to understand, I had made 'template_items' an array of numbers. But in my actual Angular project it is an array of custom objects. Here is a plunker showing that structure. It also works. And another plunker working in Angular 4.4.6
But this identical setup does not work in my Angular project. So the question stands in case anyone else can reproduce this?
In my project I get a completely empty object returned from JSON.stringify().
So, it seems I was confused between how toJSON() works and how stringify replacer function works.
With toJSON() the function you supply will ONLY return the items you specify. I was under the impression that it would return all properties and ONLY change the ones you specify in the function.
So in my project there were no template_items at the point the object was first created, and since that was the ONLY property my serialized interface specified all the other properties were being removed, hence an empty object.
So, the solution is to specify ALL properties, in both the function return statement and in the serialize interface:
toJSON(): ITemplateSerialized {
return {
id: this.id,
account_id: this.account_id,
name: this.name,
title: this.title,
info: this.info,
attributes: this.template_items
}
}
export interface ITemplateSerialized {
id: string,
account_id: string,
name: string,
title: string,
info: string,
attributes: Array<TemplateItem>
}
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.
I am trying to map my JSON file into a class object, and then update the cards based on the newly received JSON.
My JSON structure is like this
{
"$class": "FirstCard",
"id": "1",
"description": "I am card number one",
"Role": "attack",
"score": 0,
"tag": [
"string"
],................}
my Class looks like this:
class CardInfo {
//Constructor
String id;
String description;
String role;
int score;
}
How can I map the values in my JSON file into the fields of objects created from CardInfo class?
Update
the following trial prints null at ci.description, does this mean the object was never created ?
const jsonCodec = const JsonCodec
_loadData() async {
var url = 'myJsonURL';
var httpClient = createHttpClient();
var response =await httpClient.get(url);
print ("response" + response.body);
Map cardInfo = jsonCodec.decode(response.body);
var ci = new CardInfo.fromJson(cardInfo);
print (ci.description); //prints null
}
Update2
Printing cardInfo gives the following:
{$class: FirstCard, id: 1, description: I am card number one,........}
Note that it resembles the original JSON but without the double quotes on string values.
class CardInfo {
//Constructor
String id;
String description;
String role;
int score;
CardInfo.fromJson(Map json) {
id = json['id'];
description = json['description'];
role = json['Role'];
score = json['score'];
}
}
var ci = new CardInfo.fromJson(myJson);
You can use source generation tools like https://github.com/dart-lang/source_gen https://pub.dartlang.org/packages/json_serializable to generate the serialization and deserialization code for you.
If you prefer using immutable classes https://pub.dartlang.org/packages/built_value is a good bet.
If you want to get your JSON from a url do as follows:
import 'dart:convert';
_toObject() async {
var url = 'YourJSONurl';
var httpClient = createHttpClient();
var response =await httpClient.get(url);
Map cardInfo = JSON.decode(response.body);
var ci = new CardInfo.fromJson(cardInfo);
}
Please refer to the main answer if you want to know how to setup your class so that your JSON fields can be mapped to it. It is very helpful.
I created some useful library for this using reflection called json_parser which is available at pub.
https://github.com/gi097/json_parser
You can add the following to your dependencies.yaml:
dependencies:
json_parser: 0.1.1
build_runner: 0.8.3
Then the json can be parsed using:
DataClass instance = JsonParser.parseJson<DataClass>(json);
Follow the README.md for more instructions.
The best solution I've found is this medium post
Which converts the Json to dart very easily
import 'package:json_annotation/json_annotation.dart';
part 'post_model.g.dart';
#JsonSerializable()
class PostModel {
int userId;
int id;
String title;
String body;
PostModel(this.userId, this.id, this.title, this.body);
factory PostModel.fromJson(Map<String, dynamic> json) => _$PostModelFromJson(json);
Map<String, dynamic> toJson() => _$PostModelToJson(this);
}
You can generate them if you don't want to create them manually.
Add dependecies to pubspec.yaml:
dependencies:
json_annotation: ^4.0.0
dev_dependencies:
build_it: ^0.2.5
json_serializable: ^4.0.2
Create configurtion file my_classes.yaml:
---
format:
name: build_it
generator:
name: build_it:json
---
checkNullSafety: true
classes:
- name: CardInfo
fields:
- { name: id, type: String? }
- { name: description, type: String? }
- { name: role, type: String?, jsonKey: { name: Role } }
- { name: score, type: int? }
- { name: tag, type: List<String>, jsonKey: { defaultValue: [] } }
Run build process:
dart run build_runner build
Generated code my_classes.g.dart:
// GENERATED CODE - DO NOT MODIFY BY HAND
import 'package:json_annotation/json_annotation.dart';
part 'my_classes.g.g.dart';
// **************************************************************************
// build_it: build_it:json
// **************************************************************************
#JsonSerializable()
class CardInfo {
CardInfo(
{this.id, this.description, this.role, this.score, required this.tag});
/// Creates an instance of 'CardInfo' from a JSON representation
factory CardInfo.fromJson(Map<String, dynamic> json) =>
_$CardInfoFromJson(json);
String? id;
String? description;
#JsonKey(name: 'Role')
String? role;
int? score;
#JsonKey(defaultValue: [])
List<String> tag;
/// Returns a JSON representation of the 'CardInfo' instance.
Map<String, dynamic> toJson() => _$CardInfoToJson(this);
}
Now you can use them.
this pkg can help you convert JSON to a class instance. https://www.npmjs.com/package/class-converter
import { property, toClass } from 'class-convert';
class UserModel {
#property('i')
id: number;
#property()
name: string;
}
const userRaw = {
i: 1234,
name: 'name',
};
// use toClass to convert plain object to class
const userModel = toClass(userRaw, UserModel);
// you will get a class, just like below one
{
id: 1234,
name: 'name',
}
I am sending a json reponse from server in the following format:
{id: Int, name: String, childJSON: String}
and willing to map it to
export class Student{
constructor(public id: string,
public name: string,
public childJSON: ChildObject) {
}
export class ChildObject {
constructor(public class: number,
public age: number){}
on doing response.json() as Student; I am getting {id:1, name: "sumit", childJSON: "{class: 5, age: 10}" i.e. childJSON has string type instead of ChildObject type. Basically the string is not mapped to my child object. Is this the correct way to achieve it or i need to send child object from the server instead of just JSON String
You need to "re-hydrate" the objects manually in the constructor and you can't use the "parameter property" shortcut to do that (the technique where you used public in the constructor to automatically convert constructor params into class properties).
Here's how I would do it:
export class Student{
constructor(options: {
id: string;
name: string;
childJSON: any;
}) {
// Now you have to instantiate the class properties one by one.
this.id = options.id;
this.name = options.name;
this.childJSON = new ChildObject(options.childJSON);
}
}
And then to instantiate:
const student = new Student(studentJson);
Or, if you're using an Observable to fetch the data:
this.http.get(...).map(...).subscribe(studentJson =>
this.student = new Student(studentJson)
}
This solution is more flexible, as you can pass the original JSON object directly for instanciation. In your example, you had to write something like:
// NOT GOOD: You must pass each property individually, in the right order...
const student = new Student(studentJson.id, studentJson.name,...);