mcpField
F
flutter-implement-json-serialization

flutter-implement-json-serialization

github.com/dhruvanbhalara开发3050 次安装10 周前更新
代码仓库

关于 flutter-implement-json-serialization

面向 Dart 和 Flutter 开发者的 JSON 序列化指南,介绍类型安全的模型转换、后台解析,以及手动实现与代码生成的选择方法。

主要功能

  1. 01实现 fromJson 和 toJson
  2. 02使用模式匹配解析 JSON
  3. 03在后台 isolate 解析大型数据
  4. 04比较手动序列化与代码生成
  5. 05通过工作流验证序列化结果

适用场景

  1. 01为模型添加 JSON 转换方法
  2. 02解析嵌套 JSON 对象
  3. 03将大型响应移至后台解析
  4. 04决定采用手动或生成式序列化

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/dhruvanbhalara/skills --skill flutter-implement-json-serialization

安装后在 Claude Code 中输入/flutter-implement-json-serialization确认已加载。

详细介绍

目录

核心指南

  • 导入 dart:convert:手动序列化时使用 jsonEncode() 和 jsonDecode()。
  • 类型安全:始终将 jsonDecode() 的结果强制转换为 Map<String, dynamic>(对象)或 List<dynamic>(数组)。切勿直接使用原始 dynamic。
  • 封装:在模型类中定义 fromJson 工厂构造函数和 toJson 方法。
  • 后台解析:如果解析耗时超过 16ms(大型 JSON 负载),则通过 compute() 将其移至单独的 isolate。
  • 错误处理:遇到无效 JSON 时抛出 FormatException。切勿从 fromJson 返回 null。

在 fromJson 中使用 Dart 3 模式匹配

使用带解构的 switch 表达式,实现类型安全且简洁的反序列化:

import 'dart:convert';

class User {
  final int id;
  final String name;
  final String email;

  const User({required this.id, required this.name, required this.email});

  factory User.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'id': final int id,
        'name': final String name,
        'email': final String email,
      } =>
        User(id: id, name: name, email: email),
      _ => throw const FormatException('Invalid User JSON'),
    };
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'name': name,
    'email': email,
  };
}

相较于手动强制转换的优势:

  • 解构模式可在编译时进行类型检查
  • 单个表达式即可同时完成提取和验证
  • 类型不匹配时会自动抛出 FormatException
嵌套对象
class Post {
  final int id;
  final String title;
  final User author;

  const Post({required this.id, required this.title, required this.author});

  factory Post.fromJson(Map<String, dynamic> json) {
    return switch (json) {
      {
        'id': final int id,
        'title': final String title,
        'author': final Map<String, dynamic> authorJson,
      } =>
        Post(id: id, title: title, author: User.fromJson(authorJson)),
      _ => throw const FormatException('Invalid Post JSON'),
    };
  }

  Map<String, dynamic> toJson() => {
    'id': id,
    'title': title,
    'author': author.toJson(),
  };
}

后台解析

对于大型 JSON 负载(包含数千个对象),请将解析移至后台 isolate:

import 'dart:convert';
import 'package:flutter/foundation.dart';

// MUST be a top-level function (not a method or closure)
List<User> parseUsers(String responseBody) {
  final parsed = (jsonDecode(responseBody) as List<dynamic>)
      .cast<Map<String, dynamic>>();
  return parsed.map<User>((json) => User.fromJson(json)).toList();
}

Future<List<User>> fetchUsers(http.Client client) async {
  final response = await client.get(Uri.parse('https://api.example.com/users'));

  if (response.statusCode == 200) {
    return compute(parseUsers, response.body);
  } else {
    throw Exception('Failed to load users: ${response.statusCode}');
  }
}

规则:

  • 解析函数必须是顶层函数或静态方法(不能是实例方法)。
  • 简单的 isolate 任务使用 compute()。对于复杂场景,使用 Isolate.run()。
  • 阈值:解析超过 10,000 个项目或负载超过 1MB → 使用后台 isolate。

手动实现与代码生成的选择

标准手动实现(dart:convert)代码生成(json_serializable / freezed)
模型数量少于 5 个模型多于 5 个模型
嵌套深度较浅(1-2 层)深层 / 复杂层级结构
开发依赖无build_runner、json_annotation
类型安全Dart 3 模式匹配生成的代码
样板代码每个模型手动编写自动生成
构建时间无增加 build_runner 步骤
灵活性完全掌控解析过程受注解限制

建议:原型和小型模型从手动序列化开始。当模型数量超过 5 个或嵌套变得复杂时,迁移到 json_serializable 或 freezed。代码生成工作流请参阅 flutter-code-gen 技能。

工作流:实现可序列化模型

任务进度
  • 步骤 1:定义模型类,包含 final 属性和 const 构造函数。
  • 步骤 2:使用模式匹配实现 factory Model.fromJson(Map<String, dynamic> json)。
  • 步骤 3:实现 Map<String, dynamic> toJson() 方法。
  • 步骤 4:编写序列化往返单元测试(fromJson(toJson(model)) == model)。
  • 步骤 5:运行测试:dart test 或 flutter test。
  • 步骤 6:反馈循环:修复类型不匹配错误 → 重新运行,直到全部通过。

工作流:获取并解析 JSON

任务进度
  • 步骤 1:执行 HTTP 请求。
  • 步骤 2:验证响应状态码(200 → 继续,否则 → 抛出异常)。
  • 步骤 3:确定解析策略:
    • 小型负载 → 在主线程上同步解析。
    • 大型负载 → 使用 compute(parseFunction, response.body)。
  • 步骤 4:解码 JSON,并通过 fromJson 将其映射到模型。

示例

同步 HTTP 获取与解析
import 'dart:convert';
import 'package:http/http.dart' as http;

Future<User> fetchUser(http.Client client, int userId) async {
  final response = await client.get(
    Uri.parse('https://api.example.com/users/$userId'),
    headers: {'Accept': 'application/json'},
  );

  if (response.statusCode == 200) {
    final Map<String, dynamic> jsonMap =
        jsonDecode(response.body) as Map<String, dynamic>;
    return User.fromJson(jsonMap);
  } else {
    throw Exception('Failed to load user: ${response.statusCode}');
  }
}
类型安全的列表解析
Future<List<User>> fetchAllUsers(http.Client client) async {
  final response = await client.get(
    Uri.parse('https://api.example.com/users'),
  );

  if (response.statusCode == 200) {
    final List<dynamic> jsonList = jsonDecode(response.body) as List<dynamic>;
    return jsonList
        .map((e) => User.fromJson(e as Map<String, dynamic>))
        .toList();
  } else {
    throw Exception('Failed to load users');
  }
}
序列化单元测试
import 'package:flutter_test/flutter_test.dart';

void main() {
  group('$User', () {
    test('fromJson creates valid User', () {
      final json = {'id': 1, 'name': 'Alice', 'email': '[email protected]'};
      final user = User.fromJson(json);

      expect(user.id, 1);
      expect(user.name, 'Alice');
      expect(user.email, '[email protected]');
    });

    test('toJson returns valid map', () {
      const user = User(id: 1, name: 'Alice', email: '[email protected]');
      final json = user.toJson();

      expect(json['id'], 1);
      expect(json['name'], 'Alice');
    });

    test('round-trip serialization', () {
      const original = User(id: 1, name: 'Alice', email: '[email protected]');
      final json = original.toJson();
      final restored = User.fromJson(json);

      expect(restored.id, original.id);
      expect(restored.name, original.name);
      expect(restored.email, original.email);
    });

    test('throws FormatException on invalid JSON', () {
      final invalidJson = {'id': 'not_an_int', 'name': 'Alice'};
      expect(() => User.fromJson(invalidJson), throwsFormatException);
    });
  });
}

评论

支持 Markdown