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'm trying to show a list of tweets using Twitter API, using the fromJson factory.
Each tweet object has an extended_entities object which is an array of media objects.
If you're not familiar with the Twitter API you can see all the different objects here.
Here are the models i created in order to achieve this:
class Tweet {
final String createdAt;
final int id;
final String idStr;
final String text;
final String inReplyToStatusIdStr;
final String inReplyToUserIdStr;
final TweetExtendedEntities tweetExtendedEntities;
Tweet(
{this.createdAt,
this.id,
this.idStr,
this.text,
this.inReplyToStatusIdStr,
this.inReplyToUserIdStr,
this.tweetExtendedEntities});
factory Tweet.fromJson(Map<String, dynamic> json) {
return new Tweet(
createdAt: json['created_at'] as String,
id: json['id'] as int,
idStr: json['id_str'] as String,
text: json['text'] as String,
inReplyToStatusIdStr: json['in_reply_to_status_id_str'] as String,
inReplyToUserIdStr: json['in_reply_to_user_id_str'] as String,
tweetExtendedEntities: json['extended_entities'] as TweetExtendedEntities,
);
}
}
class TweetExtendedEntities {
final List<TweetMedia> tweetMedia;
TweetExtendedEntities({this.tweetMedia});
factory TweetExtendedEntities.fromJson(Map<String, dynamic> json) {
return new TweetExtendedEntities(
tweetMedia: json['media'] as List<TweetMedia>);
}
}
class TweetMedia {
final String mediaType;
final String mediaUrl;
TweetMedia({this.mediaType, this.mediaUrl});
factory TweetMedia.fromJson(Map<String, dynamic> json) {
return new TweetMedia(
mediaType: json['type'] as String,
mediaUrl: json['media_url'] as String,
);
}
}
Before i tried to get the extended_entities object everything was fine and i successfully got the JSON data and parsed it, but when i try to get the media objects using the code above, i get this error:
I/flutter (29538): type '_InternalLinkedHashMap<String, dynamic>' is not a subtype of type 'TweetExtendedEntities' in type cast where
I/flutter (29538): _InternalLinkedHashMap is from dart:collection
I/flutter (29538): String is from dart:core
I/flutter (29538): TweetExtendedEntities is from package:pubg_companion/models/tweet.dart
How can i get nested JSON objects using factory or any other way?
Dart has no idea that your JSON structure will correspond to your object, or how. You can't directly cast the JSON (which is probably a Map<String, dynamic>, but could also be other things) to your objects. #betorcs answer is a start in the right direction but needs a bit more.
This line:
tweetExtendedEntities: json['extended_entities'] as TweetExtendedEntities,
Needs to be
tweetExtendedEntities: TweetExtendedEntities.fromJson['extended_entities'],
And your TweetExtendedEntities method should look more like this:
factory TweetExtendedEntities.fromJson(Map<String, dynamic> json) {
return new TweetExtendedEntities(
tweetMedia: createTweetMediaList(json['media']));
}
static List<TweetMedia> createTweetMediaList(List json) {
if (json == null) return null;
if (json.isEmpty) return [];
return json.map((tweetMediaJson) => TweetMedia.fromJson(tweetMediaJson)).toList();
}
You could also certainly look into json_serializable if your needs start getting more complicated and you want to try to generate some of this code.
Your json parameter is Map<String, dynamic>, dynamic is not TweetExtendedEntities, but it can be cast to Map.
factory Tweet.fromJson(Map<String, dynamic> json) {
return new Tweet(
createdAt: json['created_at'] as String,
id: json['id'] as int,
idStr: json['id_str'] as String,
text: json['text'] as String,
inReplyToStatusIdStr: json['in_reply_to_status_id_str'] as String,
inReplyToUserIdStr: json['in_reply_to_user_id_str'] as String,
tweetExtendedEntities: TweetExtendedEntities.fromJson(json['extended_entities'] as Map),
);
}
In flutter, this error will be thrown when you pass a string without json.decode() to the fromjson factory
eg:
Map bodyJson = json.decode(loginResponse.body);
var login = new LoginResponse.fromJson(bodyJson);
The LoginResponse class
class LoginResponse {
int responseCode;
String message;
String responseObject;
LoginResponse();
factory LoginResponse.fromJson(Map<String, dynamic> json) => _$LoginResponseFromJson(json);
}
JSON and serialization in Flutter
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.
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>
}
Since Flutter took out dart: mirrors off of its SDK, it's no longer possible to use libraries like dartson for JSON to object serialization/deserialization. However I've read that built_value is another way of achieving a similar purpose. I couldn't find any good examples on how to implement it as it contains a significant amount of boilerplate code. Can someone give me an example? For instance, this is the JSON I'm trying to serialize to objects:
{
"name":"John",
"age":30,
"cars": [
{ "name":"Ford", "models":[ "Fiesta", "Focus", "Mustang" ] },
{ "name":"BMW", "models":[ "320", "X3", "X5" ] },
{ "name":"Fiat", "models":[ "500", "Panda" ] }
]
}
I was hoping for more details from the answers provided. Even though they were good suggestions, they were too general for me to understand. So after doing my own research, I'll share my implementation to the above JSON example I provided in hope that it would save someone's else's time. So here are the steps I followed:
In my Flutter project, first I imported the following libraries:
dependencies:
built_value: ^1.0.1
built_collection: ^1.0.0
dev_dependencies:
build_runner: ^0.3.0
built_value_generator:^1.0.1
I created a folder called tool. In it, I put 2 files: build.dart and watch.dart. There implementations of those files are show below
build.dart
// Copyright (c) 2015, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import 'dart:async';
import 'package:build_runner/build_runner.dart';
import 'package:built_value_generator/built_value_generator.dart';
import 'package:source_gen/source_gen.dart';
/// Example of how to use source_gen with [BuiltValueGenerator].
///
/// Import the generators you want and pass them to [build] as shown,
/// specifying which files in which packages you want to run against.
Future main(List<String> args) async {
await build(
new PhaseGroup.singleAction(
new GeneratorBuilder([new BuiltValueGenerator()]),
new InputSet('built_value_example', const [
'lib/model/*.dart',
'lib/*.dart',
])),
deleteFilesByDefault: true);
}
watch.dart
// Copyright (c) 2016, Google Inc. Please see the AUTHORS file for details.
// All rights reserved. Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
import 'dart:async';
import 'package:build_runner/build_runner.dart';
import 'package:built_value_generator/built_value_generator.dart';
import 'package:source_gen/source_gen.dart';
/// Example of how to use source_gen with [BuiltValueGenerator].
///
/// This script runs a watcher that continuously rebuilds generated source.
///
/// Import the generators you want and pass them to [watch] as shown,
/// specifying which files in which packages you want to run against.
Future main(List<String> args) async {
watch(
new PhaseGroup.singleAction(
new GeneratorBuilder([new BuiltValueGenerator()]),
new InputSet('built_value_example', const [
'lib/model/*.dart',
'lib/*.dart'])),
deleteFilesByDefault: true);
}
I created a serializers.dart file that would serialize my json string to my custom dart object, and my model object person.dart
serializers.dart
library serializers;
import 'package:built_collection/built_collection.dart';
import 'package:built_value/serializer.dart';
import 'package:built_value/standard_json_plugin.dart';
import 'model/person.dart';
part 'serializers.g.dart';
Serializers serializers = (
_$serializers.toBuilder()..addPlugin(new StandardJsonPlugin())
).build();
person.dart
library person;
import 'package:built_collection/built_collection.dart';
import 'package:built_value/built_value.dart';
import 'package:built_value/serializer.dart';
part 'person.g.dart';
abstract class Person implements Built<Person, PersonBuilder> {
String get name;
int get age;
BuiltList<Car> get cars;
Person._();
factory Person([updates(PersonBuilder b)]) = _$Person;
static Serializer<Person> get serializer => _$personSerializer;
}
abstract class Car implements Built<Car, CarBuilder> {
String get name;
BuiltList<String> get models;
Car._();
factory Car([updates(CarBuilder b)]) = _$Car;
static Serializer<Car> get serializer => _$carSerializer;
}
After creating the 4 files above, it will show some compiler errors. Don't mind them yet. This is because the build.dart file hasn't been run yet. So in this step, run build.dart. If you're using Webstorm, simply right click on build.dart and hit "Run build.dart". This will create 2 files: "person.g.dart" and "serializers.g.dart". If you notice carefully, in our build.dart file, we put 'lib/model/.dart' and 'lib/.dart'. The build knows where to look for those files by going through the paths specified and looks for files which have part "something" included. So it's important to keep that line in those files before running the build.dart file
Finally, now I can use the serializer in my main.dart file to serialize the json string to my custom dart object class Person. In my main.dart, I added the following code in initState()
main.dart
Person _person;
#override
void initState() {
super.initState();
String json = "{"
"\"name\":\"John\",\"age\":30,\"cars\": "
"["
"{ \"name\":\"Ford\", \"models\":[ \"Fiesta\", \"Focus\", \"Mustang\" ] },"
"{ \"name\":\"BMW\", \"models\":[ \"320\", \"X3\", \"X5\" ] },"
"{ \"name\":\"Fiat\", \"models\":[ \"500\", \"Panda\" ] }"
"]}";
setState(() {
_person = serializers.deserializeWith(
Person.serializer, JSON.decode(json));
});
}
My sample project is also available on Github Built value sample project
json_serialization
This package by the Dart Team generates everything needed for the fromJson constructor and toJson method in a seprate file.
Dependencies
Add the following dependencies:
dependencies:
json_annotation: ^2.0.0
dev_dependencies:
build_runner: ^1.0.0
json_serializable: ^2.0.0
Model class
Adapt your model class to have the following parts:
import 'package:json_annotation/json_annotation.dart';
// will be generated later
part 'person.g.dart';
#JsonSerializable()
class Person {
Person(this.name, this.age);
final String name;
final int age;
factory Person.fromJson(Map<String, dynamic> json) =>
_$PersonFromJson(json);
Map<String, dynamic> toJson() => _$PersonToJson(this);
}
Generate code
Generate the person.g.dart file from the terminal:
flutter packages pub run build_runner build
Use it
Then use it like this:
JSON → object
String rawJson = '{"name":"Mary","age":30}';
Map<String, dynamic> map = jsonDecode(rawJson);
Person person = Person.fromJson(map);
Object → JSON
Person person = Person('Mary', 30);
Map<String, dynamic> map = person.toJson();
String rawJson = jsonEncode(map);
Notes
In a Dart project use pub run build_runner build.
See this answer for more ways to serialize JSON.
From the Dart web site:
The dart:convert library provides a JsonCodec class, which you can use to convert simple types (map, list, int, num, string) automatically from a and to a JSON string. The two key static methods are JSON.encode(object) and JSON.decode(string).
Decoding example:
import 'dart:convert';
...
Map<String, dynamic> parsedMap = JSON.decode(json);
print(parsedMap['name']); // John
print(parsedMap['age']); // 30
Encoding example:
Map<String, dynamic> mapData = <String, dynamic>{ 'hello': 'world!' };
String jsonData = JSON.encode(mapData); // convert map to String
If you want to have your JSON inflate into custom Dart classes instead of a tree of primitive objects, Hadrien's answer should point you in the right direction, but I just wanted to leave this here in case anyone else is trying to get basic JSON serialization/deserialization working.
You can use Jaguar Serializer, it is easy to start and work perfectly for Flutter, or Server and Web dev.
https://github.com/Jaguar-dart/jaguar_serializer
You should prepare a configuration file for Built_value that will parse you dart source and generate the .g.dart. Once there are ready json serialisation is automatic. You can generate those files once or using a watch command.
Those file will be added at the same level than the source and the dart command
part of data;
to be seen as the same Class.
Here's the config I'm using with my Flutter project :
import 'dart:async';
import 'package:build_runner/build_runner.dart';
import 'package:built_value_generator/built_value_generator.dart';
import 'package:source_gen/source_gen.dart';
Future main(List<String> args) async {
await build(
new PhaseGroup.singleAction(
new GeneratorBuilder([
new BuiltValueGenerator(),
]),
new InputSet('flutter_project', const ['lib/data/*.dart'])),
deleteFilesByDefault: true);
}
You may find useful to read all the posts by David Morgan to understand the benefits. It need some time to turn your mind around but it's a very good pattern.
https://medium.com/dartlang/darts-built-value-for-immutable-object-models-83e2497922d4
https://medium.com/dartlang/darts-built-value-for-serialization-f5db9d0f4159
The trick is to understand how sourcegen parse and then enrich your classes by adding a lot of behaviors like Builders and Serializers.
1) first put json code to any convert website ex:
[JSON to Darthttps://javiercbk.github.io ›][1]
it will give like this output
class Autogenerated {
int? code;
List<Result>? result;
String? status;
Autogenerated({this.code, this.result, this.status});
Autogenerated.fromJson(Map<String, dynamic> json) {
code = json['code'];
if (json['result'] != null) {
result = <Result>[];
json['result'].forEach((v) {
result!.add(new Result.fromJson(v));
});
}
status = json['status'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['code'] = this.code;
if (this.result != null) {
data['result'] = this.result!.map((v) => v.toJson()).toList();
}
data['status'] = this.status;
return data;
}
}
class Result {
int? id;
int? projectId;
String? projectName;
int? userId;
String? userName;
Result(
{this.id, this.projectId, this.projectName, this.userId, this.userName});
Result.fromJson(Map<String, dynamic> json) {
id = json['id'];
projectId = json['project_id'];
projectName = json['project_name'];
userId = json['user_id'];
userName = json['user_name'];
}
Map<String, dynamic> toJson() {
final Map<String, dynamic> data = new Map<String, dynamic>();
data['id'] = this.id;
data['project_id'] = this.projectId;
data['project_name'] = this.projectName;
data['user_id'] = this.userId;
data['user_name'] = this.userName;
return data;
}
}
--------------------------------------------------------------------------------------
and change that code like this format for easy use
***get_client.dart***
import 'package:json_annotation/json_annotation.dart';
part 'get_client.g.dart';
#JsonSerializable(
explicitToJson: true,
)
class ClientModel {
#JsonKey(name: "address1")
String? address1;
#JsonKey(name: "city")
int? city;
#JsonKey(name: "city_name")
String? cityName;
#JsonKey(name: "country")
int? country;
#JsonKey(name: "country_name")
String? countryName;
#JsonKey(name: "email")
String? email;
#JsonKey(name: "first_name")
String? firstName;
#JsonKey(name: "gender")
String? gender;
#JsonKey(name: "id")
int? id;
#JsonKey(name: "last_name")
String? lastName;
#JsonKey(name: "mobile_no")
String? mobileNo;
#JsonKey(name: "password")
String? password;
#JsonKey(name: "pincode")
String? pincode;
#JsonKey(name: "role")
int? role;
#JsonKey(name: "role_name")
String? roleName;
#JsonKey(name: "state")
int? state;
#JsonKey(name: "state_name")
String? stateName;
ClientModel(
{this.address1,
this.city,
this.cityName,
this.country,
this.countryName,
this.email,
this.firstName,
this.gender,
this.id,
this.lastName,
this.mobileNo,
this.password,
this.pincode,
this.role,
this.roleName,
this.state,
this.stateName});
factory ClientModel.fromJson(Map<String, dynamic> map) =>
_$ClientModelFromJson(map);
Map<String, dynamic> toJson() => _$ClientModelToJson(this);
}
-------------------------------------------------------------------
[1]: https://%20JSON%20to%20Darthttps://javiercbk.github.io%20%E2%80%BA
after run build runner cmd in terminal
***flutter pub run build_runner build***
then it will create the following output file
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'get_client.dart';
// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************
ClientModel _$ClientModelFromJson(Map<String, dynamic> json) => ClientModel(
address1: json['address1'] as String?,
city: json['city'] as int?,
cityName: json['city_name'] as String?,
country: json['country'] as int?,
countryName: json['country_name'] as String?,
email: json['email'] as String?,
firstName: json['first_name'] as String?,
gender: json['gender'] as String?,
id: json['id'] as int?,
lastName: json['last_name'] as String?,
mobileNo: json['mobile_no'] as String?,
password: json['password'] as String?,
pincode: json['pincode'] as String?,
role: json['role'] as int?,
roleName: json['role_name'] as String?,
state: json['state'] as int?,
stateName: json['state_name'] as String?,
);
Map<String, dynamic> _$ClientModelToJson(ClientModel instance) =>
<String, dynamic>{
'address1': instance.address1,
'city': instance.city,
'city_name': instance.cityName,
'country': instance.country,
'country_name': instance.countryName,
'email': instance.email,
'first_name': instance.firstName,
'gender': instance.gender,
'id': instance.id,
'last_name': instance.lastName,
'mobile_no': instance.mobileNo,
'password': instance.password,
'pincode': instance.pincode,
'role': instance.role,
'role_name': instance.roleName,
'state': instance.state,
'state_name': instance.stateName,
};
after we can use like this in repository file
static Future<dynamic> getAllClients(int id) async {
String url = "${HttpUrls.clientList}/${0}";
Response response = await dio.get(url);
if (response.statusCode == 200) {
List<ClientModel> clientLists = (response.data['result'] as List)
.map((eachItem) => ClientModel.fromJson(eachItem))
.toList();
print(clientLists.toString());
if (clientLists.isNotEmpty) {
return clientLists;
}
} else {
BaseResponse baseResponse =
BaseResponse.fromJson(response.data as Map<String, dynamic>);
return baseResponse;
}
}