jsonflutterdartrestpatch

How to model a data object that represents a PATCH request body whose fields can be undefined, null or values


Let's suppose I need to call an HTTP API PATCH endpoint whose schema for the body is:

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

According to the schema above, all of the following bodies are valid:

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

How do I model such a data object in Dart so that when I call .toJson on that object, it outputs a Map with propertyOne set to null if the property was explicitly set to null, with propertyOne set to a string if the property was initialized with a string or excludes propertyOne if no value was specified when initializing the class?

I'm currently using freezed and have a working model for the request if the schema of the API wouldn't include null as an allowed value. How I model that is:

@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);
}

The issue comes when I want different behaviours for when a value is specifically set to null versus when a value is not specified.

For the case above, if I run PatchRequestBody(propertyOne: "test").toJson() I get { "propertyOne": "test" } which is great. If I run PatchRequestBody(propertyOne: null).toJson() it outputs an empty object, which is not great but can be fixed by removing the @JsonKey(includeIfNull: false) annotation. This unfortunately makes the case where I run PatchRequestBody(propertyTwo: "test").toJson() output { "propertyOne": null, "propertyTwo": "test" } which is not what I desire.

I tried looking for some solutions and found this Optional class from a package called quiver but it seems like they advise against not using it anymore.

Otherwise I couldn't really find a way to solve or model this.


Solution

  • If you only have a few classes to encode, you could do them by hand, for example:

    import 'dart:convert';
    
    void main() {
      print(json.encode(PatchRequestBody()));
      print(json.encode(PatchRequestBody(propertyTwo: 'test')));
      print(json.encode(PatchRequestBody(
        propertyOne: 'test',
        propertyTwo: 'test',
      )));
    }
    
    class PatchRequestBody {
      const PatchRequestBody({
        this.propertyOne,
        this.propertyTwo,
      });
    
      Map<String, dynamic> toJson() {
        if (propertyOne == null && propertyTwo == null) {
          // special case if both null - emit first property as null
          return <String, dynamic>{'propertyOne': null};
        }
        final m = <String, dynamic>{};
        _addIfNotNull(m, 'propertyOne', propertyOne);
        _addIfNotNull(m, 'propertyTwo', propertyTwo);
        return m;
      }
    
      void _addIfNotNull(Map<String, dynamic> m, String name, String? val) {
        if (val != null) {
          m[name] = val;
        }
      }
    
      final String? propertyOne;
      final String? propertyTwo;
    }