Flutter 使用 json_serializable 解析 JSON 时支持枚举 Enum 类型

2,072 阅读3分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第2天,点击查看活动详情

大家好,这篇文章向大家介绍在 Flutter 中使用 json_serializable 解析 JSON 时,如何使用枚举 Enum

JSON 数据

首先,我们先看一段 JSON 数据:

[
  {
    "type": 1,
    "text": "大家好"
  },
  {
    "type": 2,
    "image": {
      "url": "xxx",
      "height": 800,
      "width": 400
    }
  },
  {
    "type": 3,
    "audio": {
      "url": "xxx"
    }
  }
]

类似上面这段 JSON 数据,在我们实际开发中非常常见:比如订单有不同的状态,信息流中不同的类型展示不同的布局。这时如果使用枚举,可以提升代码的可读性,也便于后续维护。

定义 Model

上面那段 JSON 数据,其中的 type 字段就可以使用枚举 Enum,我们可以定义如下的模型类:

import 'package:json_annotation/json_annotation.dart';

import 'audio_model.dart';
import 'image_model.dart';

part 'message_model.g.dart';

@JsonSerializable()
class MessageModel {
  @JsonKey(
      defaultValue: MessageType.unknown, unknownEnumValue: MessageType.unknown)
  final MessageType type;
  @JsonKey(defaultValue: '')
  final String text;
  final ImageModel? image;
  final AudioModel? audio;

  const MessageModel({
    required this.type,
    required this.text,
    this.image,
    this.audio,
  });

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

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

@JsonEnum(valueField: "type")
enum MessageType {
  unknown(-1),
  text(1),
  image(2),
  audio(3);

  final int type;

  const MessageType(this.type);
}

上面代码中 MessageType 就是我们定义的枚举类,json_serializable 解析程序可以把 JSON 数据中的 1、2、3 分别映射为 MessageType.text、MessageType.image、MessageType.audio 枚举值

JsonEnum.valueField 是 json_serializable 新版本新增了一个的属性,可以用来取代原来的@JsonValue() ,我们看一下生成后代码:

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'message_model.dart';

// **************************************************************************
// JsonSerializableGenerator
// **************************************************************************

MessageModel _$MessageModelFromJson(Map<String, dynamic> json) => MessageModel(
      type: $enumDecodeNullable(_$MessageTypeEnumMap, json['type'],
              unknownValue: MessageType.unknown) ??
          MessageType.unknown,
      text: json['text'] as String? ?? '',
      image: json['image'] == null
          ? null
          : ImageModel.fromJson(json['image'] as Map<String, dynamic>),
      audio: json['audio'] == null
          ? null
          : AudioModel.fromJson(json['audio'] as Map<String, dynamic>),
    );

Map<String, dynamic> _$MessageModelToJson(MessageModel instance) =>
    <String, dynamic>{
      'type': _$MessageTypeEnumMap[instance.type]!,
      'text': instance.text,
      'image': instance.image,
      'audio': instance.audio,
    };

const _$MessageTypeEnumMap = {
  MessageType.unknown: -1,
  MessageType.text: 1,
  MessageType.image: 2,
  MessageType.audio: 3,
};

从生成后的代码中可以发现,json_serializable 会生成一个 Map ,通过这个 Map 可以把枚举值和服务端返回的 int 类型的值进行关联。

测试用例

下面我们通过一些测试用例来测试上面的模型类

import 'dart:convert';

import 'package:flutter_test/flutter_test.dart';
import 'package:json_demo/message_model.dart';

void main() {
  test('test enum type 1', () {
    String str = """
    {
      "type": 1,
      "text": "大家好"
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));

    expect(model.type, MessageType.text);
    expect(model.text, '大家好');
  });

  test('test enum type 2', () {
    String str = """
    {
      "type": 2,
      "image": {
        "url": "xxx",
        "height": 800,
        "width": 400
      }
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));

    expect(model.type, MessageType.image);
    expect(model.image?.url, 'xxx');
  });

  test('test enum type 3', () {
    String str = """
    {
      "type": 3,
      "audio": {
        "url": "yyy"
      }
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));

    expect(model.type, MessageType.audio);
    expect(model.audio?.url, 'yyy');
  });

  test('test enum type -1', () {
    String str = """
    {
      "type": -1
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));
    expect(model.type, MessageType.unknown);
  });

  test('test enum type 4', () {
    String str = """
    {
      "type": 4
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));
    expect(model.type, MessageType.unknown);
  });

  test('test enum type none', () {
    String str = """
    {
      "text": "hello"
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));
    expect(model.type, MessageType.unknown);
  });

运行单元测试代码,测试用例可以全部通过

注意事项

@JsonKey(defaultValue: MessageType.unknown, unknownEnumValue: MessageType.unknown) final MessageType type;

在 MessageModel 中,可以看到,MessageType type 被定义为非空类型,同时使用了 JsonKey 的两个属性配置,defaultValue 和 unknownEnumValue,那么为什么要同时设置两个属性呢

我们可以把这两个属性全部删掉,运行单元测试,这时有两个测试用例会报错

image.png

测试用例 test enum type 4 报错信息如下:

Invalid argument(s): `4` is not one of the supported values: -1, 1, 2, 3

测试用例 test enum type none 报错信息如下:

Invalid argument(s): A value must be provided. Supported values: -1, 1, 2, 3

默认值

当我们添加 defaultValue: MessageType.unknown

class MessageModel {
  @JsonKey(defaultValue: MessageType.unknown)
  final MessageType type;
}

test('test enum type none', () {
    String str = """
    {
      "text": "hello"
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));
    expect(model.type, MessageType.unknown);
});
  

这时 test enum type none 便不再报错,因为 JSON 中没有 type 这个参数,这时 type 应该是 null,而我们把 MessageType type 定义为非空类型,那就需要设置一个默认值来处理 null 的情况。

未知枚举值

当我们添加 unknownEnumValue: MessageType.unknown


class MessageModel {
  @JsonKey(unknownEnumValue: MessageType.unknown)
  final MessageType type;
}

test('test enum type 4', () {
    String str = """
    {
      "type": 4
    }
    """;

    MessageModel model = MessageModel.fromJson(json.decode(str));
    expect(model.type, MessageType.unknown);
});
  

test enum type 4 便不再报错,这是因为 type = 4 不是枚举类 MessageType 中定义的值,这时,就需要用到 unknownEnumValue 属性,它可以处理未识别的枚举值。

总结

在我们项目开发中,如果使用到枚举类时,我们也是按照上面讲的来做的,现在总结如下:

  1. 首先,我们在定义枚举类时,会定义一个 unknown 枚举值;
  2. 解析 JSON 的 Model 类中,把枚举类字段设置为非空类型,方便调用者,不需要做非空判断;
  3. 设置 defaultValue 属性,主要是为了容错处理;
  4. 设置 unknownEnumValue 属性,主要是为了后续升级,老版本容错处理