You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.10.26 23:54:04