'How to deserialize generic JSON with List type in Dart

I want to deserialize some JSON data that contains a list of article information

{
    "data": [
        {
            "id": 1,
            "title": "First article",
            "createdDate": "2022-03-20T11:46:00",
            "content": "Markdown content",
            "author": 1,
            "category": 1
        },
        {
            "id": 2,
            "title": "Second article",
            "createdDate": "2022-03-20T11:46:00",
            "content": "Markdown content",
            "author": 1,
            "category": 1
        }
    ]
}

No matter what the request is, the top level will have a key called data

So, I created a generic class called Entry

import 'package:json_annotation/json_annotation.dart';

part 'Entry.g.dart';

@JsonSerializable(genericArgumentFactories: true)

class Entry<TData> {
  Entry(this.data);

  TData data;

  factory Entry.fromJson(Map<String, dynamic> json,TData Function(dynamic json) fromJsonTData) => _$EntryFromJson(json,fromJsonTData);
  Map<String, dynamic> toJson(Object? Function(TData value) toJsonTData) => _$EntryToJson(this,toJsonTData);
}

And for an article, I created a class call NovelData

import 'dart:convert';
import 'dart:core';
import 'package:json_annotation/json_annotation.dart';

import 'Entry.dart';

part 'NovelData.g.dart';

@JsonSerializable(genericArgumentFactories: true)
class NovelData {
  NovelData(this.id, this.title, this.createdDate, this.content, this.author, this.category);

  int id;
  String title;
  String createdDate;
  String content;
  int author;
  int category;

  factory NovelData.fromJson(Map<String, dynamic> json) =>
      _$NovelDataFromJson(json);

  Map<String, dynamic> toJson() => _$NovelDataToJson(this);
}

Now, if I want to use the type like Entry<List<Novel>>> to deserialize the above JSON data, what should I do?



Solution 1:[1]

There is a website which automatically generates all needed code from json. Here is example:

// To parse this JSON data, do
//
//     final entry = entryFromJson(jsonString);

import 'dart:convert';

Entry entryFromJson(String str) => Entry.fromJson(json.decode(str));

String entryToJson(Entry data) => json.encode(data.toJson());

class Entry {
    Entry({
        this.data,
    });

    List<Datum> data;

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

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

class Datum {
    Datum({
        this.id,
        this.title,
        this.createdDate,
        this.content,
        this.author,
        this.category,
    });

    int id;
    String title;
    DateTime createdDate;
    String content;
    int author;
    int category;

    factory Datum.fromJson(Map<String, dynamic> json) => Datum(
        id: json["id"],
        title: json["title"],
        createdDate: DateTime.parse(json["createdDate"]),
        content: json["content"],
        author: json["author"],
        category: json["category"],
    );

    Map<String, dynamic> toJson() => {
        "id": id,
        "title": title,
        "createdDate": createdDate.toIso8601String(),
        "content": content,
        "author": author,
        "category": category,
    };
}

Solution 2:[2]

You can access them through the full path to the data.
Full path to your data: Map => key data => Array => Array index => Map
{}.data.[].0.{}
It only takes one class.

import 'package:fast_json/fast_json_selector.dart' as parser;

void main() async {
  final path = '{}.data.[].0.{}';
  final pathLevel = path.split('.').length;
  final items = <Novel>[];
  void select(parser.JsonSelectorEvent event) {
    if (event.levels.length == pathLevel) {
      if (event.levels.join('.') == path) {
        final item = Novel.fromJson(event.lastValue as Map);
        items.add(item);
        event.lastValue = null;
      }
    }
  }

  parser.parse(_json, select: select);
  print(items.join('\n'));
}

final _json = '''
{
    "data": [
        {
            "id": 1,
            "title": "First article",
            "createdDate": "2022-03-20T11:46:00",
            "content": "Markdown content",
            "author": 1,
            "category": 1
        },
        {
            "id": 2,
            "title": "Second article",
            "createdDate": "2022-03-20T11:46:00",
            "content": "Markdown content",
            "author": 1,
            "category": 1
        }
    ]
}''';

class Novel {
  final int id;
  final String title;

  Novel({required this.id, required this.title});

  @override
  String toString() {
    return title;
  }

  static Novel fromJson(Map json) {
    return Novel(
      id: json['id'] as int,
      title: json['title'] as String,
    );
  }
}

Output:

First article
Second article

You can get the data before adding it to the list. The result is no different. Just a different path to the data.

void main() async {
  final path = '{}.data.[].0';
  final pathLevel = path.split('.').length;
  final items = <Novel>[];
  void select(parser.JsonSelectorEvent event) {
    if (event.levels.length == pathLevel) {
      if (event.levels.join('.') == path) {
        final item = Novel.fromJson(event.lastValue as Map);
        items.add(item);
        event.lastValue = null;
      }
    }
  }

  parser.parse(_json, select: select);
  print(items.join('\n'));
}

This event follows the object creation event (at a lower event level):
JsonHandlerEvent.endObject => JsonHandlerEvent.element

You can get the data after adding it to the list. But it won't be as efficient.

void main() async {
  final path = '{}.data.[]';
  final pathLevel = path.split('.').length;
  final items = <Novel>[];
  void select(parser.JsonSelectorEvent event) {
    if (event.levels.length == pathLevel) {
      if (event.levels.join('.') == path) {
        final list = event.lastValue as List;
        items.addAll(list.map((e) => Novel.fromJson(e as Map)));
        list.clear();
      }
    }
  }

  parser.parse(_json, select: select);
  print(items.join('\n'));
}

JsonHandlerEvent.endObject => JsonHandlerEvent.element => JsonHandlerEvent.endArray

Or even from property data. Very inefficient because all data is stored in memory.

void main() async {
  final path = '{}.data';
  final pathLevel = path.split('.').length;
  final items = <Novel>[];
  void select(parser.JsonSelectorEvent event) {
    if (event.levels.length == pathLevel) {
      if (event.levels.join('.') == path) {
        final list = event.lastValue as List;
        items.addAll(list.map((e) => Novel.fromJson(e as Map)));
        event.lastValue = null;
      }
    }
  }

  parser.parse(_json, select: select);
  print(items.join('\n'));
}

JsonHandlerEvent.endObject => JsonHandlerEvent.element => JsonHandlerEvent.endArray => JsonHandlerEvent.endKey

I won't even write about the last level. There is no point in such an inefficient way. However, and in the previous one, too.

JsonHandlerEvent.endObject => JsonHandlerEvent.element => JsonHandlerEvent.endArray => JsonHandlerEvent.endKey => JsonHandlerEvent.endObject

Solution 3:[3]

you can try my jsonize package, it will handle any of your TData classes wherever they are in your Entry data list

import 'package:jsonize/jsonize.dart';

abstract class TData {}

class Entry implements Jsonizable {
  Entry({
    required this.data,
  });
  factory Entry.empty() => Entry(data: []);

  List<TData> data;

  @override
  String get jsonClassCode => "Entry";

  @override
  Entry fromJson(json) => Entry(data: List<TData>.from(json["data"]));

  @override
  Map<String, dynamic> toJson() => {"data": data};
}

class NovelData extends TData implements Jsonizable {
  NovelData({
    required this.id,
    required this.title,
    required this.createdDate,
    required this.content,
    required this.author,
    required this.category,
  });
  factory NovelData.empty() => NovelData(
      id: 0,
      title: "",
      createdDate: DateTime(0),
      content: "",
      author: 0,
      category: 0);

  int id;
  String title;
  DateTime createdDate;
  String content;
  int author;
  int category;

  @override
  String get jsonClassCode => "NovelData";

  @override
  NovelData fromJson(json) => NovelData(
        id: json["id"],
        title: json["title"],
        createdDate: json["createdDate"],
        content: json["content"],
        author: json["author"],
        category: json["category"],
      );

  @override
  Map<String, dynamic> toJson() => {
        "id": id,
        "title": title,
        "createdDate": createdDate,
        "content": content,
        "author": author,
        "category": category,
      };
}

main() {
  Jsonize.registerClass(Entry.empty());
  Jsonize.registerClass(NovelData.empty());

  NovelData novel1 = NovelData(
      id: 1,
      title: "First article",
      createdDate: DateTime.now(),
      content: "Markdown content",
      author: 1,
      category: 1);
  NovelData novel2 = NovelData(
      id: 2,
      title: "Second article",
      createdDate: DateTime.now(),
      content: "Markdown content",
      author: 1,
      category: 1);
  Entry myEntry = Entry(data: [novel1, novel2]);

  String myEntryJson = Jsonize.toJson(myEntry);
  print(myEntryJson);
  Entry entryBackToLife = Jsonize.fromJson(myEntryJson);
  print(entryBackToLife);
}

jsonize can do more like handling enums. In your case benefits are:

  1. DateTime serialization is handled by jsonize, you don not need to transform it
  2. You can derive new classes from TData and put them into your Entry, jsonize will handle them and automatically transform it back to the original class

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Behzod Faiziev
Solution 2
Solution 3 cabbi