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

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

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

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

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

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

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

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

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

前言

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

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

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

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

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

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

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

  • 统一的代码风格规范。

客户端工具说明

使用

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

格式化

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

更多设置

设置会全部自动保存。

数据类型全方位保护

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

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

dart 复制代码
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整个解析失败的情况,大家遇到过吧?

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

dart 复制代码
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类信息

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

第一列是在 json 中对应的 key

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

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

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

生成Dart

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

DartJsonToDart

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

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

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

dart 复制代码
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 对象。

dart 复制代码
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 来重新设置参数。

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

通用配置项

参数名 类型 默认值 说明
addMethod bool true 是否生成 asT 和数组循环保护方法。
enableArrayProtection bool false 是否在解析数组时启用保护机制
enableDataProtection bool false 是否在解析数据(如 null 或格式不合法)时启用保护
fileHeaderInfo String '' 每个生成的 Dart 文件顶部添加的自定义注释头
traverseArrayCount int 1 检查数组时遍历的元素数量
propertyNamingConventionsType PropertyNamingConventionsType camelCase 属性命名规范,例如 camelCase、snake_case 等
propertyAccessorType PropertyAccessorType none 属性访问器风格,如是否使用 getter/setter
propertyNameSortingType PropertyNameSortingType none 属性名称是否进行排序
nullsafety bool true 是否启用 Dart 的空安全特性
nullable bool true 属性是否允许为 null
smartNullable bool false 是否启用智能可空推断
addCopyMethod bool false 是否生成 copyWith 方法
automaticCheck bool true 是否在生成前自动检测潜在问题
showResultDialog bool true 是否在转换完成后显示结果弹窗
equalityMethodType EqualityMethodType official 控制是否生成 ==hashCode 或者是 Equatable 形式
deepCopy bool false copyWith 方法是否支持对象的深拷贝
formatter DartFormatter? 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"
propertyCantSameAsClassName String 属性的名字不能跟类名一样 'property can\'t the same as Class name'
keywordCheckFailed(name) String 返回关键词冲突错误信息,如:'name' is a key word!'
propertyCantSameAsType String 属性的名字不能跟类型一样 'property can\'t the same as Type'
containsIllegalCharacters String 包含非法的字符 'contains illegal characters'
duplicateProperties String 有重复的属性 'There are duplicate properties'
duplicateClasses String 有重复的类 'There are duplicate classes'

控制器说明

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

  • 定义新的控制器
dart 复制代码
class MyJsonToDartController with JsonToDartControllerMixin {}
  • 注册新的控制器
dart 复制代码
  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> allProperties Set<DartProperty> 存储当前解析过程中的所有属性信息,用于去重等判断
Set<DartObject> allObjects Set<DartObject> 存储当前解析过程中所有生成的类对象,用于去重等判断
Set<DartObject> printedObjects Set<DartObject> 存储已打印输出过的对象,避免重复输出类定义

结语

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

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

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

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

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

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

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

相关推荐
BoomHe9 分钟前
Android 源码两种执行脚本的区别
android·源码
Kapaseker17 分钟前
Jetpack Compose的副作用一览
android·kotlin
szhangbiao17 分钟前
RxJava炒冷饭之实用案例
android·rxjava
Wgllss1 小时前
6种Kotlin中单例模式写法,特点及应用场景指南
android·架构·android jetpack
prinTao2 小时前
【代码解析】opencv 安卓 SDK sample - 1 - HDR image
android·人工智能·opencv
_一条咸鱼_4 小时前
Android Runtime内存分配与对象生命周期深度解析(57)
android·面试·android jetpack
tbit4 小时前
dart私有命名构造函数的作用与使用场景
flutter·dart
玲小珑4 小时前
Auto.js 入门指南(十八)常见问题与解决方案
android·前端
ExistOrLive4 小时前
ZLGithubClient 1.8.0 版本功能说明
ios·github