Dart 中如何惯用化解析JSON为共享基类、异构数据结构的对象?
泛型化通用响应包装实现
现有方案的冗余核心是:每个接口都需要单独定义一个继承自基类的响应子类,仅为了绑定对应类型的data字段,属于完全可避免的样板代码。借助Dart泛型+json_serializable的泛型工厂能力,仅需定义一次通用响应结构,即可覆盖所有接口的响应场景,直接将需要定义的类数量减半。
1. 全局通用错误类定义
因为所有接口的error结构完全一致,仅需定义一次即可,无需随接口重复编写:
@JsonSerializable() class ApiError { // 按实际服务端返回的error字段调整,如code、message、traceId等 final int code; final String message; ApiError({required this.code, required this.message}); factory ApiError.fromJson(Map<String, dynamic> json) => _$ApiErrorFromJson(json); }
2. 通用泛型响应类实现
开启json_serializable的genericArgumentFactories配置(类注解中直接声明即可),支持泛型字段的动态解析,这个类全局仅写一次:
// genericArgumentFactories: true 是泛型解析的必要配置 @JsonSerializable(createToJson: false, genericArgumentFactories: true) class ApiResponse<T> { ApiResponse({this.data, this.error}); // data的类型由泛型T指定,无需为每个接口单独写类绑定 T? data; ApiError? error; // 泛型解析需要传入T对应的反序列化方法,无法自动生成 factory ApiResponse.fromJson( Map<String, dynamic> json, T Function(Object? json) fromJsonT, ) => _$ApiResponseFromJson(json, fromJsonT); }
3. 业务接口类定义对比
原有实现每个接口需要写2个类:
- 继承基类的空壳响应子类(如
ExampleResponse) - 对应data结构的数据类(如
ExampleResponseData)
改造后,完全不需要那个空壳响应子类,仅需定义和接口data结构对应的业务类即可:
// 直接定义和接口返回data结构对应的类,无额外包装 @JsonSerializable() class Thing { final String name; Thing(this.name); factory Thing.fromJson(Map<String, dynamic> json) => _$ThingFromJson(json); } @JsonSerializable() class ExampleData { final Thing thing; ExampleData({required this.thing}); factory ExampleData.fromJson(Map<String, dynamic> json) => _$ExampleDataFromJson(json); }
使用时直接绑定泛型即可完成解析:
// 拿到接口返回的原始JSON后 final rawJson = jsonDecode(httpResponseBody); final response = ApiResponse<ExampleData>.fromJson( rawJson, // 传入data对应的反序列化方法 (json) => ExampleData.fromJson(json as Map<String, dynamic>), ); // 按状态处理即可 if (response.error != null) { // 走错误处理逻辑 } else { final thingName = response.data!.thing.name; }
4. 可选优化:封装通用请求逻辑
可以在SDK内部封装统一的请求方法,把泛型解析的重复逻辑收起来,业务层调用时无需重复写解析模板:
// SDK内部通用请求封装 Future<ApiResponse<T>> sendRequest<T>( String path, T Function(Map<String, dynamic> json) dataDeserializer, ) async { // 执行网络请求获取原始JSON final rawJson = await performHttpRequest(path); return ApiResponse<T>.fromJson( rawJson, (jsonVal) => dataDeserializer(jsonVal as Map<String, dynamic>), ); }
业务层调用时仅需传入对应的数据类解析方法即可,没有额外样板代码:
final resp = await sdk.sendRequest( '/example/path', (json) => ExampleData.fromJson(json), );
如果遇到data是数组的场景,同样可以用泛型支持,无需调整通用响应类:
// 接口data返回Thing列表的场景 final resp = await sdk.sendRequest<List<Thing>>( '/things/list', (json) => (json['items'] as List).map( (item) => Thing.fromJson(item as Map<String, dynamic>) ).toList(), );
对于结构简单、没有复用价值的data场景,可以直接在解析方法中返回基础类型、Map或临时解析结果,实现和Go匿名结构体一致的效果,无需单独定义顶层类。
5. 可选增强:类型安全的密封响应类
如果需要避免空值判断、明确区分成功/错误状态,可以把通用响应类改造为密封类,同样仅需定义一次,无需随接口调整:
sealed class ApiResponse<T> { const ApiResponse(); factory ApiResponse.fromJson( Map<String, dynamic> json, T Function(Object? json) fromJsonT, ) { // 按服务端结构判断走成功还是错误分支 if (json['error'] != null) { return ApiErrorResponse(ApiError.fromJson(json['error'])); } return ApiSuccessResponse(fromJsonT(json['data'])); } } // 成功状态,data非空 class ApiSuccessResponse<T> extends ApiResponse<T> { final T data; const ApiSuccessResponse(this.data); } // 错误状态,error非空 class ApiErrorResponse<T> extends ApiResponse<T> { final ApiError error; const ApiErrorResponse(this.error); }
使用时通过类型判断即可获取对应字段,无需冗余的空值检查:
switch (response) { case ApiSuccessResponse(data: final data): print(data.thing.name); case ApiErrorResponse(error: final err): print(err.message); }
内容的提问来源于stack exchange,提问作者Ryan Haley
相关产品推荐
相关产品推荐

