Flutter parsing multilevel JSON to a class using sound null safety

Viewed 2814

I use FLutter in Android studio, parsing multilevel JSON to a class. Although I have checked many stackoverflow post I can not find a solution for this problem. This is my JSON

String myJson = '
[
 {
  "sensor":"1234",
  "data":
   [
    {
     "value":"40",
     "reading_time1":"2021-04-10"
    },
    {
     "value":"38",
     "reading_time1":"2021-04-11"
     }
   ]
 },
 {
  "sensor":"1235",
  "data":
   [
    {
     "value":"2",
     "reading_time1":"2021-04-23"
    },
    {
     "value":"39",
     "reading_time1":"2021-04-24"
    }
   ]
 }
] '

These are my classes, generated using https://javiercbk.github.io/json_to_dart/ . But the generated code did not work (not null safety) so after reading many post I came up with these:

class AllSensorData {

  String sensor="";
  List<Sensordata> sensordata = [];

  AllSensorData({required this.sensor,required this.sensordata});

  AllSensorData.fromJson(Map<String, dynamic> json) {
    sensor = json['sensor'];
    if (json['data'] != null) {
      sensordata = <Sensordata>[];
      json['data'].forEach((v) {
        sensordata.add(new Sensordata.fromJson(v));
      });
    }
  }

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['sensor'] = this.sensor;
    if (this.sensordata != null) {
      data['data'] = this.sensordata.map((v) => v.toJson()).toList();
    }
    return data;
  }

}

class Sensordata {
  String value;
  String reading_time;

  Sensordata({
    required this.value,
    required this.reading_time
  });

  Sensordata.fromJson(Map<String, dynamic> json) :
    value = json['value'],
    reading_time = json['reading_time1'];

  Map<String, dynamic> toJson() {
    final Map<String, dynamic> data = new Map<String, dynamic>();
    data['value'] = this.value;
    data['reading_time'] = this.reading_time;
    return data;
  }
}

Now when I run:

var decodedJson = jsonDecode(myJson);
var jsonValue = AllSensorData.fromJson(decodedJson);

The error is:

type 'List<dynamic>' is not a subtype of type 'Map<String, dynamic>'

Although I suspect there might be more. Thanks for help.

4 Answers

This is a slightly more generic answer for other people coming here.

Analyzer options

When you have implicit-dynamic set to false in your analysis_options.yaml file, this can make it hard to parse JSON at first.

analyzer:
  strong-mode:
    implicit-casts: false
    implicit-dynamic: false

How to caste

If you are working with a list at the top level of your JSON, you can use as List to caste the type:

import 'dart:convert';

void main() {
  final myList = json.decode(myJson) as List;
  for (final item in myList) {
    final name = item['name'] as String? ?? '';
    final age = item['age'] as int? ?? 0;
  }
}

String myJson = '''
[
  {
    "name":"bob",
    "age":22
  },
  {
    "name":"mary",
    "age":35
  }
]
''';

That allows you to loop though each item. The item itself is dynamic, but you can still treat it as a Map and give it a default typed value if it is null.

Using a fromJson method

Here is an example using a data class. (The import and myJson string are omitted for brevity.)

void main() {
  final myList = json.decode(myJson) as List;
  for (final item in myList) {
    if (item is! Map<String, dynamic>) return; //      <-- interesting part
    final person = Person.fromJson(item);
  }
}

class Person {
  final String name;
  final int age;

  Person(this.name, this.age);

  factory Person.fromJson(Map<String, dynamic> json) {
    final name = json['name'] as String? ?? '';
    final age = json['age'] as int? ?? 0;
    return Person(name, age);
  }
}

This one does mostly the same thing, except before passing the map into the fromJson method, you check the type. That allows Dart to safely promote the dynamic item to the Map<String, dynamic> type needed by the fromJson parameter.

Your JSON data is a List<dynamic> hence decodedJson is also List<dynamic>.

You have to decode the json from each and every element of decodedJson.

  var decodedJson = jsonDecode(myJson);
  List<AllSensorData> list = [];
  for (Map<String, dynamic> json in decodedJson) {
    list.add(AllSensorData.fromJson(json));
  }

If you have any doubt comment it.

As others have pointed out, the JSON has a list of sensor data, so you need to decode the list elements. I use quicktype to generate the (correct) code.

You say you are using null safety, but you do not have any optional data fields in your classes. If the fields in the data are optional, then you can use the quicktype option Make all properties optional. Unfortunately quicktype does not seem to know about null safety in dart, so you have to edit its generated code to add ? on the data field declarations, and ! on the references. I did this below:

import 'dart:convert';

List<Sensordata> sensordataFromJson(String str) =>
    List<Sensordata>.from(json.decode(str).map((x) => Sensordata.fromJson(x)));

String sensordataToJson(List<Sensordata> data) =>
    json.encode(List<dynamic>.from(data.map((x) => x.toJson())));

class Sensordata {
  Sensordata({
    this.sensor,
    this.data,
  });

  String? sensor;
  List<Datum>? data;

  factory Sensordata.fromJson(Map<String, dynamic> json) => Sensordata(
        sensor: json["sensor"] == null ? null : json["sensor"],
        data: json["data"] == null
            ? null
            : List<Datum>.from(json["data"].map((x) => Datum.fromJson(x))),
      );

  Map<String, dynamic> toJson() => {
        "sensor": sensor == null ? null : sensor,
        "data": data == null
            ? null
            : List<dynamic>.from(data!.map((x) => x.toJson())),
      };
}

class Datum {
  Datum({
    this.value,
    this.readingTime1,
  });

  String? value;
  DateTime? readingTime1;

  factory Datum.fromJson(Map<String, dynamic> json) => Datum(
        value: json["value"] == null ? null : json["value"],
        readingTime1: json["reading_time1"] == null
            ? null
            : DateTime.parse(json["reading_time1"]),
      );

  Map<String, dynamic> toJson() => {
        "value": value == null ? null : value,
        "reading_time1": readingTime1 == null
            ? null
            : "${readingTime1!.year.toString().padLeft(4, '0')}-${readingTime1!.month.toString().padLeft(2, '0')}-${readingTime1!.day.toString().padLeft(2, '0')}",
      };
}

Your code is mostly correct. You just need to call fromJson for each element of decodedJson since it contains many sensorMaps not one.

List<Map<String,dynamic>> decodedJson = jsonDecode(myJson);

List<AllSensorData> sensorDataList = decodedJson.forEach(
  (sensorMap) => AllSensorData.fromJson(sensorMap)
);

You'll do the same for while calling toJson.

Related