Handling nested JSON in Dart

foreword

Due to some standards ( JSON and serialization ) formulated by the Dart language itself , it is impossible to find a library similar to Gson in Dart. The official description is as follows:

Is there an equivalent of GSON/Jackson/Moshi in Flutter?
Simply put, no.

Such libraries need to use runtime for reflection, which is disabled in Flutter. Runtime reflection affects tree shaking (an optimization) that Dart has supported for quite some time. With tree shaking, you can "shake" unused code from your distribution. This will significantly optimize the size of the App.

This makes tree shaking difficult since reflection makes all code used implicitly by default. Tools don't know which parts won't be used at runtime, so redundant code is hard to clean up. App size cannot be easily optimized when using reflection.

However, using json_serializable is a bit cumbersome. Later, I decided to write a conversion class by myself according to the habits of libraries such as gson.

At least initially achieve the purpose of processing nested JSON with the json-to-dart-model plug -in in VSCode .

generate entity classes

If the JSON source data is as follows (actually, it is the free joke data of the aggregation platform ):

{
    
    
    "error_code": 0,
    "reason": "Success",
    "result": {
    
    
        "data": [
            {
    
    
                "content": "有一天晚上我俩一起吃西瓜,老大把西瓜籽很整洁的吐在了一张纸上,\r\n过了几天,我从教室回但宿舍看到老大在磕瓜子,\r\n我就问他:老大,你什么时候买的瓜子?\r\n老大说:刚晒好,说着抓了一把要递给我……",
                "hashId": "bcc5fdc2fb6efc6db33fa242474f108a",
                "unixtime": 1418814837,
                "updatetime": "2014-12-17 19:13:57"
            },
            {
    
    
                "content": ""我女朋友气跑了"\r\n"怎么回事?严重吗?你怎么着她了?"\r\n"不严重,我只是很久没用了"",
                "hashId": "03a6095c18e1d6fe7e2c19b2a20d03d1",
                "unixtime": 1418814837,
                "updatetime": "2014-12-17 19:13:57"
            },
            {
    
    
                "content": "还说神马来一场说走就走的旅行,\r\n工作后就连一场说走就走的下班都不行。",
                "hashId": "10edf75c1e7d0933c91f0f39a28a2c84",
                "unixtime": 1418814837,
                "updatetime": "2014-12-17 19:13:57"
            }
        ]
    }
}

Then use the json-to-dart-model to convert the entity class to generate the following three classes;
the outermost JuheResponse:

class JuheResponse{
    
    
  int? errorCode;
  String? reason;
  Result? result;

  JuheResponse({
    
    this.errorCode, this.reason, this.result});

  factory JuheJoke.fromJson(Map<String, dynamic> json) => JuheResponse(
        errorCode: json['error_code'] as int?,
        reason: json['reason'] as String?,
        result: json['result'] == null
            ? null
            : Result.fromJson(json['result'] as Map<String, dynamic>),
      );

  Map<String, dynamic> toJson() => {
    
    
        'error_code': errorCode,
        'reason': reason,
        'result': result?.toJson(),
      };
}

Result of the second layer:

import 'datum.dart';

class Result {
    
    
  List<Datum>? data;

  Result({
    
    this.data});

  factory Result.fromJson(Map<String, dynamic> json) => Result(
        data: (json['data'] as List<dynamic>?)
            ?.map((e) => Datum.fromJson(e as Map<String, dynamic>))
            .toList(),
      );

  Map<String, dynamic> toJson() => {
    
    
        'data': data?.map((e) => e.toJson()).toList(),
      };
}

The last layer of data Datum, in order to reflect the difference, later changed the class name to Joke:

class Datum {
    
    
  String? content;
  String? hashId;
  int? unixtime;
  String? updatetime;

  Datum({
    
    this.content, this.hashId, this.unixtime, this.updatetime});

  factory Datum.fromJson(Map<String, dynamic> json) => Datum(
        content: json['content'] as String?,
        hashId: json['hashId'] as String?,
        unixtime: json['unixtime'] as int?,
        updatetime: json['updatetime'] as String?,
      );

  Map<String, dynamic> toJson() => {
    
    
        'content': content,
        'hashId': hashId,
        'unixtime': unixtime,
        'updatetime': updatetime,
      };
}

If there is only one data entity like Joke in the application, it is actually no problem to use the above classes to convert;
but the problem lies in the actual situation, different Result may be nested under the JuheResponse class , and there will be different Datum in the Result ; According to the experience of JSON sequence libraries under other language platforms, we need to introduce generics and create JuheResponse<T> and Result<T>;

Introducing generics

For example, change the fromJson method in JuheResponse to:

  factory JuheResponse.fromJson(Map<String, dynamic> json) => JuheResponse(
        errorCode: json['error_code'] as int?,
        reason: json['reason'] as String?,
        result: json['result'] == null
            ? null
            : JSON().fromJson(json['result'] as Map<String, dynamic>,T) as T,
      );

Change the fromJson method in Result to:

  factory Result.fromJson(Map<String, dynamic> json) => Result(
        data: (json['data'] as List<dynamic>?)
            ?.map((e) => JSON().fromJson(e as Map<String, dynamic>,T) as T)
            .toList(),
      );

In this way, no matter what class is nested, it can be successfully assembled into the proper entity.
Then in the preliminary idea, the JSON tool class should have such a method:

///map或string转model实体,使用后需用as T转换相应的对象类型
  dynamic fromJson(dynamic value, Type type) {
    
    
	// ...
    switch (type) {
    
    
      case JuheResponse<Result<Joke>>:
        return JuheResponse<Result<Joke>>.fromJson(value);
      case Result<Joke>:
        return Result<Joke>.fromJson(value);
      case Joke:
        return Joke.fromJson(value);
    }
    // ...
  }

Because Dart's generic type checking is stricter than Java, it is necessary to declare an accurate class name ;

Considering that the value passed in in the entity class is usually Map<String, dynamic> , which is decoded by JSON, we can judge whether the value passed in is the original string before that, and convert it once if it is:

    if (value is String) {
    
    
      //字符串需经过json解码为map格式
      value = json.decode(value);
    }

In this way, the original string can be passed in;
in addition, the data in the form of List<T> can be processed by using map to convert one by one and finally combined into a List. It should be noted that
if the type passed in is List< T>, then further processing should be done:

      case List<Joke>:
        return (value as List).map<Joke>((e) => fromJson(e, Joke)).toList();

That is, the data type of each step is declared;
in this way, the Result of the second layer can also be changed accordingly, and List<Datum> can be changed to T:

class Result<T> {
    
    
  T? data;

  Result({
    
    this.data});

  factory Result.fromJson(Map<String, dynamic> json) => Result(
        data: JSON().fromJson(json['data'], T) as T,
      );

  Map<String, dynamic> toJson() => {
    
    
        'data': JSON().toJson(data, false),
      };
}

The corresponding toJson also gives similar treatment.

final tool class

After considering the conversion of strings, maps, and Lists, the final tool class is as follows:

class JSON {
    
    
  factory JSON() => _instance;

  JSON._internal();

  static final JSON _instance = JSON._internal();

  ///map或string转model实体,使用后需用as T转换相应的对象类型
  dynamic fromJson(dynamic content, Type type) {
    
    
    var value;
    if (content is String) {
    
    
      //字符串需经过json解码为map格式
      value = json.decode(content);
      if (value is List) {
    
    
        return fromJson(value, type);
      }
    } else if (content is Map<String, dynamic>) {
    
    
      //map形式直接使用
      value = content;
    } else if (content is List) {
    
    
      //List则嵌套调用自身即可
      value = content;
    } else {
    
    
      throw const FormatException("content is not json format");
    }

    switch (type) {
    
    
      case JuheResponse<Result<Joke>>:
        return JuheResponse<Result<Joke>>.fromJson(value);
      case Result<Joke>:
        return Result<Joke>.fromJson(value);
      case List<Joke>:
       	return (value as List).map<Joke>((e) => fromJson(e, Joke)).toList();
      case Joke:
        return Joke.fromJson(value);
      default:
        throw const FormatException("no model provided for content");
    }
    // return json;
  }

  ///实体转map或string
  dynamic toJson(dynamic data, bool bejson) {
    
    
    var result = bejson ? '' : Map<String, dynamic>();
    if (data is List) {
    
    
      return data.map((e) => toJson(e, bejson)).toList();
    } else {
    
    
      switch (data.runtimeType) {
    
    
        case Joke:
          result = (data as Joke).toJson();
          break;
        case Result<Joke>:
          result = (data as Result<Joke>).toJson();
          break;
        case JuheResponse<Result<Joke>>:
          result = (data as JuheResponse<Result<Joke>>).toJson();
          break;
        default:
          print("no model provided toJson");
      }
    }
    if (bejson) {
    
    
      result = json.encode(result);
    }
    return result;
  }
}

have a test:

    Future(() async {
    
    
      return await rootBundle.loadString("/data/jokes.json");
    }).then((value) {
    
    
      JuheResponse<Result<Joke>> r =
          JSON().fromJsonAs<JuheResponse<Result<Joke>>>(value);
      setState(() {
    
    
        jokes = r.result!.data!;
        print(jokes[2].content);
        var s = JSON().toJson(jokes, false);
        //print(s);
        var jj = JSON().fromJson(s, List<Joke>);
        print(jj.length);
      });
    });

that's all.

Guess you like

Origin blog.csdn.net/ifmylove2011/article/details/125887129