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

如何在Dart中建模区分null与未指定字段的PATCH请求体?

处理Dart Freezed中区分“显式null”与“未指定字段”的JSON序列化问题

问题背景

某HTTP API的PATCH请求体JSON Schema如下:

{
  "type": "object",
  "properties": {
    "propertyOne": { "type": ["string", "null"] },
    "propertyTwo": { "type": "string" }
  }
}

合法的请求体示例:

{ "propertyOne": null }
{ "propertyTwo": "test" }
{ "propertyOne": "test", "propertyTwo": "test" }

序列化需求

  • 若propertyOne被显式设为null,JSON输出需包含"propertyOne": null;
  • 若propertyOne被初始化为字符串,输出对应键值对;
  • 若初始化类时未指定propertyOne,JSON中不包含该字段。

现有方案的问题

当前使用Freezed库的实现如下:

@freezed
class PatchRequestBody with _$PatchRequestBody {
  const factory PatchRequestBody({
      @JsonKey(includeIfNull: false) String? propertyOne,
      @JsonKey(includeIfNull: false) String? propertyTwo
    }) = _PatchRequestBody;

  factory PatchRequestBody.fromJson(Map<String, dynamic> json) =>
      _$PatchRequestBodyFromJson(json);
}

存在以下问题:

  • 调用PatchRequestBody(propertyOne: null).toJson()会输出空对象,显式null被忽略;
  • 移除@JsonKey(includeIfNull: false)后,调用PatchRequestBody(propertyTwo: "test").toJson()会输出{ "propertyOne": null, "propertyTwo": "test" },未指定的字段被转为null,不符合需求。

解决方案

通过自定义可选值包装类配合JsonConverter,实现“未指定”与“显式null”的区分:

1. 实现可选值包装类

class Optional<T> {
  final T? value;
  final bool isProvided;

  // 显式提供值(包括null)
  const Optional.provided(this.value) : isProvided = true;
  // 未提供值
  const Optional.unprovided()
      : value = null,
        isProvided = false;
}

2. 实现JSON转换器

class OptionalConverter<T> implements JsonConverter<Optional<T>, dynamic> {
  const OptionalConverter();

  @override
  Optional<T> fromJson(dynamic json) {
    // 反序列化时,只要存在字段就标记为已提供
    return Optional.provided(json as T?);
  }

  @override
  dynamic toJson(Optional<T> optional) {
    // 未提供时返回null,配合includeIfNull: false忽略该字段
    if (!optional.isProvided) return null;
    // 已提供时返回实际值(可能为null)
    return optional.value;
  }
}

3. 修改Freezed模型

@freezed
class PatchRequestBody with _$PatchRequestBody {
  const factory PatchRequestBody({
    @OptionalConverter()
    @JsonKey(includeIfNull: false)
    Optional<String>? propertyOne,

    // propertyTwo是必填字符串,直接用required
    required String propertyTwo,
  }) = _PatchRequestBody;

  factory PatchRequestBody.fromJson(Map<String, dynamic> json) =>
      _$PatchRequestBodyFromJson(json);
}

4. 使用示例

  • 未指定propertyOne:
    PatchRequestBody(propertyTwo: "test").toJson();
    // 输出: {"propertyTwo": "test"}
    
  • 显式设置propertyOne为null:
    PatchRequestBody(
      propertyOne: const Optional.provided(null),
      propertyTwo: "test"
    ).toJson();
    // 输出: {"propertyOne": null, "propertyTwo": "test"}
    
  • 设置propertyOne为字符串:
    PatchRequestBody(
      propertyOne: const Optional.provided("test"),
      propertyTwo: "test"
    ).toJson();
    // 输出: {"propertyOne": "test", "propertyTwo": "test"}
    

内容的提问来源于stack exchange,提问作者Codrin

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.22 15:43:14