JsonToDart,你已经是一个成熟的工具了,接下来就靠你自己继续进化了!

2,172 阅读4分钟

六年,一个工具,一段成长

世界一直在变,人类能从那么多生物中脱颖而出,很大一部分原因就是——我们会用工具。

六年前,我写了一个小工具,叫 Json to Dart。当时只是想省点手动写 model 的功夫,没想到这一做就是六年。

这六年里,从微软的 SilverlightWPFUWP,到谷歌的 Flutter;从熟悉的 C# 转向 Dart;还记得最开始写这个工具的时候,Dart 还是 2.x 版本,后来 Dart 推出了空安全(null safety)等特性,我也陆续做了适配和重构,一路跟着 Dart 语言演进,工具也跟着我一起不断成长。

现在,它已经发布了超过 30 个版本,服务了不少开发者。

这个工具,见证了我从 .NET 世界跨入移动开发的整个过程,也陪我经历了无数次写完就想重构的夜晚。

说到底,它只是个工具,但对我来说,是六年技术旅程的缩影。

希望未来还能继续一起进化。

前言

说到当时做这个工具的目的:

  • 老是被服务端坑,各种数据类型瞎返回。

  • 参数命名混乱,强迫症根本不能忍。

  • 生成 copyWith, 相等判断(hashCode,== 或者 Equatable) 等常见方法。

  • 避免 Dart 关键字,避免类名重复。

  • 避免对象数组缺失字段(后端返回对象的字段可能不全)。

  • 数组解析避免一个解析错误报错,整个列表报错。

  • 统一的代码风格规范。

客户端工具说明

使用

屏幕截图 2025-06-22 115153.png

左边是 json 的输入框,右边是生成的 json 类的结构

格式化

点击格式化按钮,将 json 转换为右边可视化的 json 类结构

更多设置

屏幕截图 2025-06-22 115253.png

设置会全部自动保存。

数据类型全方位保护

大家一定会有被服务端坑的时候吧? 不按规定好了的数据类型传值,导致 json 整个解析失败。

打开这个开关,就会在获取数据的时候加一层保护,当然你可以自己定义这个方法,默认代码如下:

T? asT<T extends Object?>(dynamic value, [T? defaultValue]) {
  if (value is T) {
    return value;
  }
  try {
    if (value != null) {
      final String valueS = value.toString();
      if (String == T || '' is T) {
        return valueS as T;
      } else if (int == T || 0 is T) {
        return int.parse(valueS) as T;
      } else if (double == T || 0.0 is T) {
        return double.parse(valueS) as T;
      } else if (bool == T || false is T) {
        if (valueS == '0' || valueS == '1') {
          return (valueS == '1') as T;
        }
        return (valueS == 'true') as T;
      } else {
        return FFConvert.convert<T>(value);
      }
    }
  } catch (e, stackTrace) {
    log('asT<$T>', error: e, stackTrace: stackTrace);
    return defaultValue;
  }

  return defaultValue;
}

class FFConvert {
  FFConvert._();
  static T? Function<T extends Object?>(dynamic value) convert = <T>(
    dynamic value,
  ) {
    if (value == null) {
      return null;
    }
    return json.decode(value.toString()) as T?;
  };
}

说到 asT,最近还被 ChatGPT 坑了一下。原来类型不一致的时候,我是这样去判断的 0 is T,这样运行是没有问题的(Dart 混淆了也能通过),但我最近重构的时候又问了下 ChatGPT, 它给我的答案是 int == T ,但是这个写法有一个问题,那就是如果用户写的是 asT<int?> 的话,int == T 是不通过的。这让我想到一个程序员的梗,运行稳定的代码不要轻易去重构

数组全方位保护

在循环数组的时候,一个出错,导致json整个解析失败的情况,大家遇到过吧?

打开这个开关,将对每一次循环解析进行保护,代码如下

void tryCatch(Function f) {
  try {
    f?.call();
  } catch (e, stack) {
    debugPrint("$e");
    debugPrint("$stack");
  }
}

遍历数组次数

在服务器返回的数据中,数组里面不是每一个 item 都带有全部的属性,

如果只检查第一个话,会存在属性丢失的情况

你可以通过多次循环来避免丢失属性

选项有 1,20,99

99 就代表循环全部进行检查

属性命名

Dart 命名规范

属性命名规范选项:

  • 保持原样
  • 驼峰式命名小驼峰 josnToDart
  • 帕斯卡命名大驼峰 JsonToDart
  • 匈牙利命名下划线 json_to_dart

Dart 官方推荐 驼峰式命名小驼峰

属性排序

对属性进行排序

排序选项:

  • 保持原样
  • 升序排列
  • 降序排序

添加保护方法

是否添加保护方法。数据类型全方位保护/数组全方位保护 这 2 个开启的时候会生成方法。 第一次使用的时候开启就可以了,你可以方法提出去放一个 dart 文件里面(并且在文件头中加入引用)。 后面生成的时候就没必要再开启了。

文件头部信息

可以在这里添加 copyrightimprot,创建人信息等等,支持[Date yyyy MM-dd]来生成时间,Date 后面为日期格式。

比如 [Date yyyy MM-dd] 会将你生成 Dart 代码的时间按照 yyyy MM-dd 的格式生成对应时间

修改json类信息

image.png

点击格式化之后,右边会显示可视化的json类结构。

第一列是在 json 中对应的 key

第二列为属性类型/类的名字。如果是类名,会用蓝色箭头表示

第三列是属性的名字,输入选项如果为空,会报红提示

第四列是属性的访问器类型

生成Dart

image.png

做好设置之后,点击生成,就会自动将生成的代码复制到裁剪板,当然你也可以从弹框中复制。

DartJsonToDart

客户端工具虽然功能丰富,但是没法做更精细的自定义。 我们需要做得更多,比如可以根据项目的设计,直接读取后端的文档,生成数据模型和接口请求方法。

为此,我将 JsonToDart 的解析部分抽离成单独的纯 Dart 库 (fluttercandies/json_to_dart_library: json_to_dart_library)。

我们可以通过自定义配置控制器和代码生成过程,来自定义属于自己的 JsonToDart

import 'dart:io';

import 'package:json_to_dart_library/json_to_dart_library.dart';

Future<void> main(List<String> args) async {
  registerConfig(MyJsonToDartConfig());
  registerController(MyJsonToDartController());

  DartObject? dartObject = await jsonToDartController.jsonToDartObject(
    json: '''{"data":[{"a":1}],"msg":"s","code":0}''',
  );
  var errors = jsonToDartController.getErrors();
  if (errors.isNotEmpty) {
    print('Errors found:');
    for (var error in errors) {
      print(error);
    }
    return;
  }

  if (dartObject != null) {
    var dartCode = jsonToDartController.generateDartCode(dartObject);
    File('output.dart').writeAsStringSync(dartCode!);
    print('Dart code generated successfully:');
  }
}

class MyJsonToDartConfig extends JsonToDartConfig {
  @override
  bool get addMethod => true;
  @override
  bool get enableArrayProtection => true;
  @override
  bool get enableDataProtection => true;

  @override
  int get traverseArrayCount => 99;

  @override
  bool get nullable => true;

  @override
  bool get nullsafety => true;

  // @override
  // bool get smartNullable => true;
  @override
  DartObject createDartObject({
    required String uid,
    required int depth,
    required MapEntry<String, dynamic> keyValuePair,
    required bool nullable,
    DartObject? dartObject,
  }) {
    return MyDartObject(
      uid: uid,
      depth: depth,
      keyValuePair: keyValuePair,
      nullable: nullable,
      dartObject: dartObject,
    );
  }

  @override
  DartProperty createProperty({
    required String uid,
    required int depth,
    required MapEntry<String, dynamic> keyValuePair,
    required bool nullable,
    required DartObject dartObject,
  }) {
    return MyDartProperty(
      uid: uid,
      depth: depth,
      keyValuePair: keyValuePair,
      nullable: nullable,
      dartObject: dartObject,
    );
  }
}

class MyJsonToDartController with JsonToDartControllerMixin {}

class MyDartObject extends DartObject {
  MyDartObject({
    required super.uid,
    required super.depth,
    required super.keyValuePair,
    required super.nullable,
    super.dartObject,
  });

  @override
  String toString() {
    // 做一些自己的事情
    return super.toString(); 
  }
}

class MyDartProperty extends DartProperty {
  MyDartProperty({
    required super.uid,
    required super.depth,
    required super.keyValuePair,
    required super.nullable,
    required DartObject super.dartObject,
  });
}

DartObject/DartProperty

json 会被转换成 DartObject/DartProperty 对象,里面存储着信息,通过DartObjecttoString 方法 得到整个 json 转换的 dart 数据模型代码。你可以通过重写这 2 个类来修改生成的 dart 数据模型代码,并且在下一步的配置中指定新的 DartObject/DartProperty 对象。

class MyDartObject extends DartObject {
  MyDartObject({
    required super.uid,
    required super.depth,
    required super.keyValuePair,
    required super.nullable,
    super.dartObject,
  });

  @override
  String toString() {
    // 做一些自己的事情
    return super.toString(); 
  }
}

class MyDartProperty extends DartProperty {
  MyDartProperty({
    required super.uid,
    required super.depth,
    required super.keyValuePair,
    required super.nullable,
    required DartObject super.dartObject,
  });
}

配置项说明

你可以通过重载 JsonToDartConfig 来重新设置参数。

  • 定义新的配置
class MyJsonToDartConfig extends JsonToDartConfig {
  @override
  bool get addMethod => true;
}
  • 注册新的配置
 registerConfig(MyJsonToDartConfig());

通用配置项

参数名类型默认值说明
addMethodbooltrue是否生成 asT 和数组循环保护方法。
enableArrayProtectionboolfalse是否在解析数组时启用保护机制
enableDataProtectionboolfalse是否在解析数据(如 null 或格式不合法)时启用保护
fileHeaderInfoString''每个生成的 Dart 文件顶部添加的自定义注释头
traverseArrayCountint1检查数组时遍历的元素数量
propertyNamingConventionsTypePropertyNamingConventionsTypecamelCase属性命名规范,例如 camelCase、snake_case 等
propertyAccessorTypePropertyAccessorTypenone属性访问器风格,如是否使用 getter/setter
propertyNameSortingTypePropertyNameSortingTypenone属性名称是否进行排序
nullsafetybooltrue是否启用 Dart 的空安全特性
nullablebooltrue属性是否允许为 null
smartNullableboolfalse是否启用智能可空推断
addCopyMethodboolfalse是否生成 copyWith 方法
automaticCheckbooltrue是否在生成前自动检测潜在问题
showResultDialogbooltrue是否在转换完成后显示结果弹窗
equalityMethodTypeEqualityMethodTypeofficial控制是否生成 ==hashCode 或者是 Equatable 形式
deepCopyboolfalsecopyWith 方法是否支持对象的深拷贝
formatterDartFormatter?DartFormatter(latestLanguageVersion)格式化生成的 Dart 代码,控制缩进、风格等

对象创建方法

当你需要重载 DartObjectDartProperty 的时候,你需要重载这 2 个方法,返回你自定义的 DartObjectDartProperty

方法名返回类型说明
createProperty({...})DartProperty创建新的 DartProperty,用于生成属性模型。参数包括 uiddepthkeyValuePairnullabledartObject
createDartObject({...})DartObject创建新的 DartObject,用于生成类结构模型。参数包括 uiddepthkeyValuePairnullabledartObject?

错误信息与断言

返回的错误信息,你可以在这里支持国际化。

名称类型示例值 / 描述
propertyNameAssert(uid)String返回属性名为空的提示信息,如:"$uid: property name is empty"
classNameAssert(uid)String返回类名为空的提示信息,如:"$uid: class name is empty"
propertyCantSameAsClassNameString属性的名字不能跟类名一样 'property can\'t the same as Class name'
keywordCheckFailed(name)String返回关键词冲突错误信息,如:'name' is a key word!'
propertyCantSameAsTypeString属性的名字不能跟类型一样 'property can\'t the same as Type'
containsIllegalCharactersString包含非法的字符 'contains illegal characters'
duplicatePropertiesString有重复的属性 'There are duplicate properties'
duplicateClassesString有重复的类 'There are duplicate classes'

控制器说明

你可以通过混入 JsonToDartControllerMixin 来重新重载方法。

  • 定义新的控制器
class MyJsonToDartController with JsonToDartControllerMixin {}
  • 注册新的控制器
  registerController(MyJsonToDartController());

方法说明

方法名返回类型说明
Future<DartObject?> jsonToDartObject({ required String json, String rootObjectName = 'Root' })Future<DartObject?>JSON 字符串解析为 DartObject 对象结构,处理空安全和配置项
DartObject? dynamicToDartObject(dynamic jsonData, { String rootObjectName = 'Root' })DartObject?将动态 JSON 数据(Map/List)转换为 DartObject,自动适配结构
String? generateDartCode(DartObject? dartObject)String?根据 DartObject 生成 Dart 代码,支持插入 header、导入语句、空安全等
List<String> getErrors()List<String>收集所有 DartObjectDartProperty 中的错误信息列表,用于展示/校验
void handleError(Object? e, StackTrace stack)void错误处理函数,打印错误并抛出异常(支持统一调试)

成员变量说明

成员名类型说明
Set<DartProperty> allPropertiesSet<DartProperty>存储当前解析过程中的所有属性信息,用于去重等判断
Set<DartObject> allObjectsSet<DartObject>存储当前解析过程中所有生成的类对象,用于去重等判断
Set<DartObject> printedObjectsSet<DartObject>存储已打印输出过的对象,避免重复输出类定义

结语

通过将 JsonToDart 的解析部分抽离成单独的纯 Dart 库 (fluttercandies/json_to_dart_library: json_to_dart_library),现在大家都可以依赖这个库去做自己的脚本或者客户端了!

JsonToDart 接下来就靠你自己继续进化了!

工具对人类最大的意义,不在于“让事变得更容易”,而在于拓展了我们的能力边界

  • 用石头砸核桃,是早期的“硬件工具”;
  • 用算盘计算,是认知能力的延伸;
  • 用编程语言写出工具,是“思维的工具”。

从打火石、车轮,到现代的 AI、自动化工具,它们共同帮助人类从“靠自己”做事,变成“设计系统”做事,工具在进化,人也在进化!

Flutter,爱糖果,欢迎加入Flutter Candies,一起生产可爱的Flutter小糖果

最最后放上 Flutter Candies 全家桶,真香。