Flutter parsing multilevel JSON to a class using sound null safety

2,653

Solution 1

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')}",
      };
}

Solution 2

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.

Solution 3

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.

Share:
2,653
user7394563
Author by

user7394563

Updated on December 29, 2022

Comments

  • user7394563
    user7394563 over 1 year

    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.

    • Afridi Kayal
      Afridi Kayal almost 3 years
      decodedJson is actually a list of SensorData and not just one. You are trying to pass a list to AllSensorData.fromJson whereas it accepts a map and not a list. Hence the error. Try AllSensorData(decodedJson[0]) after ensuring there is at least one element in there.
  • user7394563
    user7394563 almost 3 years
    Thanks for the answer. I tried to use the code, but during the runtime I get an error of: The following _TypeError was thrown while handling a gesture: type 'Null' is not a subtype of type 'String' debugging this line: list.add(AllSensorData.fromJson(json));
  • Abdur Rafay Saleem
    Abdur Rafay Saleem almost 3 years
    That's because in your class method fromJson you didn't specify a return type.
  • Dhritiman Roy
    Dhritiman Roy almost 3 years
    This error occurs when one of the fields in the AllSensorData is given a value of null from the API. You need to check if any value is null or not in the fromJson method and return according to that.