Flutter JSON parsing in constructor - json

I'm trying to parse a JSON file from the web, and while I can parse most of the fields, I can't seem to pass some of them inside the constructor. Some come out fine, the others come out as null.
This is my constructor
Event({
this.title,
this.comment,
});
And this is my factory constructor which decodes the JSON of each event:
factory Event.fromJson(Map<String, dynamic> json) {
String s = json['EVENT_COMMENT'];
print('event comment: $s'); //this prints well, gives me the exact comment of the event found in the JSON file
return Event(
title: json['EVENT_TITLE'], //fine, gives me the exact title of the event in the JSON file
comment: s, // when I try to print the whole list of events, with toString() method overridden, this comes out as null
);
}
This is my toString() method overridden in this same class (when I call this method on this class's object, it returns title fine, whereas comment is null):
#override
String toString() {
return '$title, $comment';
}
I have never had this weird thing happen to me, I have almost always been able to successfully parse JSON in Dart. Am I missing something here?
(Spared most of the details so that it is straightforward)
Edit: This is one of the JSON objects I'm parsing (all of them have the same fields):
{
...
"EVENT_COMMENT": "This is a cool event.",
"EVENT_TITLE": "Some event title here.",
...
},
Edit 2: this one also returns null when called, for some reason:
String getComment() => comment;

Related

Eliminate duplicate Json elements and retrieve element names starting with capital letters spring boot/java

I'm developing a Rest Client using Spring Boot and Spring Framework (spring-boot-starter-parent 2.1.6.RELEASE)
I have a class representing a response object as shown below:
public class ValidateResponse {
private String ResponseCode;
private String ResponseDesc;
//getters and setters
//constructors using fields
//empty constructor
}
I'm creating a web-hook for an external api and I need to return a JSON object to for a specific endpoint (the JSON object properties must start with uppercase(s)). I'm calling returning the object from a PostMapping method nested in a RequestMapping root path:
#PostMapping("hooks/validate")
public ValidateResponse responseObj(#RequestHeader Map<String, String> headersObj) {
ValidateResponse response = new ValidateResponse("000000", "Success");
logger.info("Endpoint = hooks/validate | Request Headers = {}", headersObj);
return response;
}
However, when I hit the endpoint from postman I'm getting duplicate varialbes
{
"ResponseCode": "000000",
"ResponseDesc": "Success",
"responseCode": "000000",
"responseDesc": "Success"
}
I understand that the pojo-json conversion is handled by spring but I don't understand why the conversion is yielding duplicate variables.
Note: I know the ResponseDesc and the ResponseCode are not declared using the best standards for naming variables (camelCasing).
I've done some digging and according to the Java Language Specification
An identifier is an unlimited-length sequence of Java letters and Java digits, the first of which must be a Java letter.
and
The "Java letters" include uppercase and lowercase ASCII Latin letters A-Z (\u0041-\u005a), and a-z (\u0061-\u007a), and, for historical reasons, the ASCII underscore (_, or \u005f) and dollar sign ($, or \u0024). The $ character should be used only in mechanically generated source code or, rarely, to access pre-existing names on legacy systems.
So, I'm assuming its syntactically correct to define a variable using the Camelcase format [Need clarification on this].
I'm considering having to create the JSON object manually but I'd like to know the cause of this behaviour first. Any pointers are appreciated.
Jackson deserializes all the public fields that it comes across. However if you want Jackson to return the response in your expected element names (in your case elements starting with capital letters), make the fields private and annotate them with the #JsonProperty(expected_name_here). Your class file will typically looks as shown below
public class ValidateResponse {
#JsonProperty("ResponseDesc")
private String responseCode;
#JsonProperty("ResponseDesc")
private String responseDesc;
//getters and setters
//constructors using fields
//empty constructor
}
Note: The getters and setters for these fields should be public, otherwise Jackson won't see anything to deserialize in the class.
public class ValidateResponse {
#JsonProperty("ResponseDesc")
public String responseCode;
#JsonProperty("ResponseDesc")
public String responseDesc;
//getters and setters
//constructors using fields
//empty constructor
}
This must fix your problem, however I do not know the reason as it requires deep Jackson investigation.
EDIT
I found out the reason.
The field got duplicated because in you case you had:
2 public fields named in upper case -> they are to be processed by jackson
2 getters getResponseCode and getResponseDesc -> they are to be resolved
as accessors for properties responseCode and responseDesc accordingly.
Summing this up - you have 4 properties resolved by Jackson. Simply making your fields private will resolve your issue, however I still advise using JsonProperty approach.
I added a com.google.code.gson dependency in the projects pom.xml file to configure Spring Boot to use Gson (instead of the default jackson).
The Json object returned from the hooks/validate endpoint must have its property names starting with a capital letter. Using a java class to generate the response object was resulting to camelCased property names so I resolved to create the JSON response object manually. Here's the code for creating the custom JSON object:
public ResponseEntity<String> responseObj(#RequestHeader Map<String, String> headersObj) {
HttpHeaders responseHeaders = new HttpHeaders();
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
JsonObject response = new JsonObject();
response.addProperty("ResponseCode", "00000000");
response.addProperty("ResponseDesc" , "Success");
logger.info("Endpoint = hooks/validate | Request Headers = {}", headersObj);
return ResponseEntity.ok().headers(responseHeaders).body(response.toString());
}
Note The JSON object is returned as a String so the response from the endpoint must have an additional header to define MediaType to inform the calling system that the response is in JSON format:
responseHeaders.setContentType(MediaType.APPLICATION_JSON);
then add the header to the response:
return ResponseEntity.ok().headers(responseHeaders).body(response.toString());

How to encode an object to json in Flutter

I am trying to convert the object "Week" to json.
https://flutter.dev/docs/development/data-and-backend/json this is the source that i used
class Week{
DateTime _startDate;
DateTime _endDate;
List<Goal> _goalList;
String _improvement;
Week(this._startDate, this._endDate){
this._goalList = List<Goal>();
this._improvement = "";
}
Week.fromJson(Map<String, dynamic> json)
: _startDate = json['startDate'],
_endDate = json['endDate'],
_goalList = json['goalList'],
_improvement = json['improvement'];
Map<String, dynamic> toJson() =>
{
'startDate': _startDate,
'endDate': _endDate,
'goalList': _goalList,
'improvement': _improvement,
};
}
I used this:
DateTime startDate = currentDate.subtract(new Duration(days:(weekday-1)));
DateTime endDate = currentDate.add(new Duration(days:(7-weekday)));
Week week = new Week(startDate, endDate);
var json = jsonEncode(week);
But the problem is that I get this result:
Unhandled Exception: Converting object to an encodable object failed: Instance of 'Week'
#0 _JsonStringifier.writeObject (dart:convert/json.dart:647:7)
#1 _JsonStringStringifier.printOn (dart:convert/json.dart:834:17)
#2 _JsonStringStringifier.stringify (dart:convert/json.dart:819:5)
#3 JsonEncoder.convert (dart:convert/json.dart:255:30)
#4 JsonCodec.encode (dart:convert/json.dart:166:45)
#5 jsonEncode (dart:convert/json.dart:80:10)
jsonEncode requires a Map<String, dynamic>, not a Week object. Calling your toJson() method should do the trick.
var json = jsonEncode(week.toJson());
However, keep in mind that your toJson() method is also incorrect, as things like _goalList and the dates are still objects, not Maps or Lists. You'll need to implement toJson methods on those as well.
To answer your specific questions:
Because dart is not javascript / typescript. Dart checks types at runtime, therefore you have to explicitly tell it how to convert things - also there is no reflection in dart, so it can't figure it out by itself.
You can use a library that uses code generation to do these things automatically for you - it still wont be possible at runtime though - read more about JSON serialization.
The easiest way would be to implement the methods directly in the classes, as that's where you have access to in your root object. Keep in mind that the structure that jsonEncode needs is a Map<String, dynamic>, but the dynamic part really means List<dynamic>, Map<String, dynamic> or a primitive that is json compatible such as String or double - if you try to imagine how such a nested structure of said types looks, you'll realise that it's basically json. So when you do something like 'goalList': _goalList, you're giving it an object, which is not one of the allowed types.
Hope this clears things up a bit.
for anyone wondering: I got my solution.
To make my code work I needed to implement the toJson() methods at my class Goal as well (because I used List<Goal> in Week).
class Goal{
String _text;
bool _reached;
Map<String, dynamic> toJson() =>
{
'text': _text,
'reached': _reached,
};
}
Also, I needed to add .toIso8601String() to the DateTime objects like that in the Week class:
Map<String, dynamic> toJson() =>
{
'startDate': _startDate.toIso8601String(),
'endDate': _endDate.toIso8601String(),
'goalList': _goalList,
'improvement': _improvement,
};
Now the output is:
{"startDate":"2019-05-27T00:00:00.000Z","endDate":"2019-06-02T00:00:00.000Z","goalList":[],"improvement":""}
Taking suggestion 2 from #Phillip's answer above for Json serialization, the Freezed package I believe you can skip the #JsonSerializable annotation and just used the #Freezed annotation because Freezed "will automatically ask json_serializable to generate all the necessary fromJson/toJson."
So the example here: https://flutter.dev/docs/development/data-and-backend/json#use-code-generation-for-medium-to-large-projects
becomes:
//import 'package:json_annotation/json_annotation.dart';
import 'freezed_annotation/freezed_annotation.dart';
/// This allows the `User` class to access private members in
/// the generated file. The value for this is *.g.dart, where
/// the star denotes the source file name.
part 'user.g.dart';
part 'user.freezed.dart';
/// An annotation for the code generator to know that this class needs the
/// JSON serialization logic to be generated.
#freezed
class User {
User(this.name, this.email);
String name;
String email;
/// A necessary factory constructor for creating a new User instance
/// from a map. Pass the map to the generated `_$UserFromJson()` constructor.
/// The constructor is named after the source class, in this case, User.
factory User.fromJson(Map<String, dynamic> json) => _$UserFromJson(json);
/// `toJson` is the convention for a class to declare support for serialization
/// to JSON. The implementation simply calls the private, generated
/// helper method `_$UserToJson`.
Map<String, dynamic> toJson() => _$UserToJson(this);
}
Freezed: https://pub.dev/packages/freezed
Don't forget to edit pubspec.yaml for freezed and freezed_annotation

Typescript class with default values, how to parse JSON to this

I have a class of type A.
This class has several properties, let's call them prop1, prop2 and prop3.
When I'm calling an API, that returns a JSON string representing the object, some properties might be omitted if they are null. Further down the road, however, this object is used to construct a form dynamically (using Formik, but that's unrelated).
This framework expects all properties to be there, and some will be visible dynamically depending on other properties.
So my question, how can I parse a JSON response to my custom class, keeping default values in case properties are omitted in the API response?
What I've tried was:
static getCustomer(id) {
return fetch(process.env.MD_API_URL + 'customers/' + id, { mode: 'cors' })
.then(response => {
let cust = new Customer();
return response.json().then(x => cust = JSON.parse(x));
}).catch(error => {
return error;
});
}
But this returns undefined. Must be doing something wrong...
since typescript is not actually compiled but translated into javascript so all the javascript rules apply.
Therefore deserializing json wont actually create a new instance of the class in question but gives you an object you can "call" Customer during design time.
you could however create an object and then assign the json values like this:
export class Customer {
public id: number;
public name: string;
// your stuff here
public myDefaultProp: string = "default value";
public constructor(init?: Partial<Customer>) {
Object.assign(this, init);
}
}
your return then would look like this:
return response.json().then(x => new Customer(JSON.parse(x)));
added an example https://stackblitz.com/edit/typescript-16wlmg
This essentially just a matter of determining what to do in order to create an instance of a class, and map the properties of a JSON response towards your custom class, and there could be many different ways to solve this,
But I think (Factory function) is appropriate approach for this kind of task.

My RCP Client is not returning a deep copy of an object

I have been working on an RCP Client to handle weather data.
What i do is 2 things, first i scraped the JSON i will be using and put it into a dart file. See: https://dartpad.dartlang.org/a9c1fe8ce34c608eaa28
My server.dart page, will import the weather data, and then carry out the following:
import "dart:io";
import "weather_data.dart";
import "dart:convert";
import "package:rpc/rpc.dart";
find ApiServer _apiServer = new ApiServer(prettyPrint:true);
main() async {
Weather w = new Weather(WeatherJson);
TestServer ts = new TestServer(w);
_apiServer.addApi(ts);
HttpServer server = await HttperServer.bind(InternetAddress.ANY_IP_V4, 12345);
server.listen(_apiServer.httpRequestHandler);
}
class Weather{
Map weather;
Weather(this.weather){
Map get daily => weather["daily"];
}
}
#ApiClass(name:"test_server", version: 'v1', description: 'This is a test server api to ping for some quick sample data.')
class TestServer {
Weather myWeather;
TestServer(this.myWeather){
}
#ApiMethod(method:'GET', path: 'daily')
Map<String, Object> getDaily(){
return myWeather.daily;
}
}
So, the server starts correctly, and i will go to localhost:12345/test_server/v1/daily and it will return this:
{
"summary": {},
"icon": {},
"data": {}
}
which is not correct. If you look up the JSON data, summary and icon are both strings and data is an array. They are also empty, and should contain the data i wanted to return.
Why does this occur? Is it because i am returning a Map<String, Object>? I was trying to set it up to be: Map<String, dynamic> but the dart compiler didnt like it.
How do i get this data to return the correct dataset?
The Dart website for RPC is located at: https://github.com/dart-lang/rpc
and you can see that under methods, the return value of a method can be either an instance of a class or a future. That makes sense as per usual, so I set it to be a Map<String,Object> though trying to be vague about it by saying: Map was not sufficient.
Edit:
When doing this mostly in dart pad without RPC, it seems to work correctly, by a sample of: https://dartpad.dartlang.org/3f6dc5779617ed427b75
This leads me to believe something is wrong with the Parsing tool as it seems the return type in dartpad allows to return Map, Map<String, Object>, and Map<String, dynamic>.
Having had a quick look at the RPC package README here https://pub.dartlang.org/packages/rpc, it seems that methods marked as Api methods (with #ApiMethod) should return an instance of a class with simple fields such as:
class ResourceMessage {
int id;
String name;
int capacity;
}
The RPC package will take that instance and serialize it into JSON based on the field names.
From the README:
The MyResponse class must be a non-abstract class with an unnamed
constructor taking no required parameters. The RPC backend will
automatically serialize all public fields of the the MyResponse
instance into JSON ...
You are returning a nested Map representation of the JSON you want the RPC operation to emit and would guess that the RPC package does not handle it as you are expecting it to.
Re: this from your question:
This leads me to believe something is wrong with the Parsing tool as
it seems the return type in dartpad allows to return Map, Map, and Map.
There is no 'parsing' on JSON going on on your example. The data you have is a set of nested literal Dart Maps, Lists and Strings with the same structure as the JSON it was derived from. It just happens to look like JSON.
In your example you are just selecting and printing a sub-map of your data map (data['daily']), which prints out the String that results from calling toString() - which is recursive so you get the contents of all the nested maps and lists within it.
So it's not a 'deep copy' issue, but a difference in how toString() and the RPC code processes a set of nested maps.
BTW: the return type of your getDaily() method is immaterial. What is returned is just a Map whatever the declared return type of the method is. Remember types in Dart are optional and there for editors and compilers to spot potentially incorrect code. See https://www.dartlang.org/docs/dart-up-and-running/ch02.html#variables.
I am going to piggyback off of #Argenti Apparatus here as there was a lot of information gained from him.
Long story short, the required return type of the method:
#ApiMethod(method:'GET', path: 'daily')
Map<String,Object> getDaily(){ // <-- Map<String,Object>
return myWeather.daily;
}
is the error.
I went through and updated the method signature to be Map<String,String> and it parsed it entirely correct. It did not parse the object as a string, but actually parsed it as a full recursed object.
I went through and for the sake of code cleanliness also changed signatures of Weather properties to reflect what they actually were, Map<String,Object> as well.
All in all, When defining it to be an value type of Object, it was returning curly braces, but setting it as a String parsed it correctly.
I ran it through JSLint to confirm it is correct as well.
I gave a +1 to the helper, I had to dig deeper into the code to see WHY it wasnt doing a Map correctly.
This also I feel, is plausibly a bug in RPC Dart.

How to convert an object containing DateTime fields to JSON in Dart?

I try to convert an object to JSON.
var obj = { "dt": new DateTime.now() };
var s = stringify(obj);
The runtime throws an exception: "Calling toJson method on object failed."
That's expected since DateTime class doesn't have toJson method.
But what should I do in this case?
Javascript's JSON.stringify function has an optional argument replacer which allows me to provide my own way of serialization of any object even if the object has no toJson method. Is there any similar facility in Dart or maybe I can extend somehow DateTime class with my own toJson method?
JSON conversion only works with maps, lists, strings, numbers, booleans, or null. So what if your object contains another type like DateTime?
DateTime → JSON
Let's start with the following object:
class Person {
Person(this.name, this.birthdate);
String name;
DateTime birthdate;
}
You can convert it to a map like this:
final person = Person('Bob', DateTime(2020, 2, 25));
Map<String, dynamic> map = {
'name': person.name,
'birthdate': person.birthdate,
};
If you tried to encode this right now with jsonEncode (or json.encode), you would get an error because the DateTime is not directly serializeable. There are two solutions.
Solution 1
You could serialize it yourself first like this:
Map<String, dynamic> map = {
'name': person.name,
'birthdate': person.birthdate.toIso8601String(),
};
final jsonString = json.encode(map);
Note:
Here is the difference between toString and toIso8601String:
2020-02-25 14:44:28.534 // toString()
2020-02-25T14:44:28.534 // toIso8601String()
The toIso8601String doesn't have any spaces so that makes it nicer for conversions and sending over APIs that might not deal with spaces well.
Solution 2
You could use the optional toEncodable function parameter on jsonEncode.
import 'dart:convert';
void main() {
final person = Person('Bob', DateTime(2020, 2, 25));
Map<String, dynamic> map = {
'name': person.name,
'birthdate': person.birthdate,
};
final toJson = json.encode(map, toEncodable: myDateSerializer);
}
dynamic myDateSerializer(dynamic object) {
if (object is DateTime) {
return object.toIso8601String();
}
return object;
}
The toEncodable function just converts the input to a string or something that jsonEncode can covert to a string.
JSON → DateTime
There is nothing special here. You just have to parse the string into the type that you need. In the case of DateTime you can use its parse or tryParse methods.
final myMap= json.decode(jsonString);
final name = myMap['name'];
final birthdateString = myMap['birthdate'];
final birthdate = DateTime.parse(birthdateString);
final decodedPerson = Person(name, birthdate);
Note that parse will throw an exception if the format of the string cannot be parsed into a DateTime object.
As a model class
class Person {
Person(this.name, this.birthdate);
String name;
DateTime birthdate;
Person.fromJson(Map<String, dynamic> json)
: name = json['name'],
birthdate = DateTime.tryParse(json['birthdate']),
Map<String, dynamic> toJson() {
return {
'name': name,
'birthdate': birthdate.toIso8601String(),
};
}
}
This will not throw an exception is the date is malformatted, but birthdate would be null.
Notes
See my fuller answer here.
Thanks to this answer for pointing me in the right direction.
Zdeslav Vojkovic's answer is outdated now.
The JSON.encode() method in dart:convert has an optional toEncodable method that is invoked for objects that are not natively serializable to JSON. It's then up to the user to provide a closure that returns an appropriate serialization of the DateTime.
IMO, it's a flaw in dart:json library that stringify doesn't support additional callback to serialize types lacking the implementation of toJson. Strangely enough, parse function does support reviver argument to customize the deserialization process. Probably the reason was along the lines that user can add toJson for their on types, and core library will take care of 'native' types, but DateTime was omitted (maybe because date serialization in JSON is not really a straightforward story).
Maybe the goal of the team was to use custom types instead of Maps (which is nice), but the drawback here is that if your custom type contains 10 other properties and 1 which is of DateTime type, you need to implement toJson which serializes all of them, even integers and strings. Hopefully, once when Mirror API is ready, there will be libraries that implement serialization 'out-of-band' by reading the reflected info on type, see lower for an example. The promise of Dart is that I would be able to use my types both on server and client, but if I have to manually implement serialization/deserialization for all my models then it is too awkward to be usable.
it also doesn't help that DateTime itself is not very flexible with formatting, as there are no other methods besides toString which returns the format useless for serialization.
So here are some options:
wrap (or derive from) DateTime in your own type which provides toJson
patch json.stringifyJsonValue and submit the change or at least submit an issue :)
use some 3-rd party library like json-object (just an example, it also doesn't support DateTime serialization, AFAIK
I am not really happy with any of them :)
I've added a new package to Dart pub.dev that allows json serialization of objects within a structure. This package Jsonize serialize and deserialize custom classes, and handles DateTime out of the box:
List<dynamic> myList = [1, "Hello!", DateTime.now()];
var jsonRep = Jsonize.toJson(myList);
var myDeserializedList = Jsonize.fromJson(jsonRep);
So will do with your example
var obj = { "dt": new DateTime.now() };
var s = Jsonize.toJson(obj);
var obj2 = Jsonize.fromJson(s);
but can do also this
var obj = DateTime.now();
var s = Jsonize.toJson(obj);
var dt = Jsonize.fromJson(s);