Skip to content

Add opt-in freezed_nullable_presence for PATCH null semantics - #478

Open
Winget-Software wants to merge 2 commits into
Carapacik:mainfrom
Winget-Software:feat/patch-presence-flags
Open

Winget-Software wants to merge 2 commits into
Carapacik:mainfrom
Winget-Software:feat/patch-presence-flags

Conversation

@Winget-Software

Copy link
Copy Markdown

Closes #477.

This PR adds an opt-in freezed_nullable_presence option (default false). For Freezed models with JSON serialization enabled, it generates a <name>Present flag next to each optional nullable property. With the flag, toJson can tell an omitted key from an explicit null. A PATCH body needs that difference to clear a field without sending null for every other unset field. It is the Dart counterpart of OpenAPI Generator's openApiNullable / JsonNullable<T>. #477 has the background and links.

With the option enabled:

  • Constructor: gets @Default(false) bool <name>Present for each property that is not required and is nullable (or references a nullable schema).
  • Annotation: @Freezed(fromJson: false, toJson: false) together with a private const X._() constructor.
  • fromJson/toJson: both are generated by hand. They delegate value conversion to a private @JsonSerializable() helper class (_XJson), so enum, date, nested-model and @JsonKey handling stay with json_serializable. fromJson sets each flag from json.containsKey(key). For a property that is null, toJson writes null when the flag is set and removes the key otherwise.
  • JSON keys: use protectJsonKey, the same as @JsonKey(name: ...).

The option is ignored, and the current output is generated, when any of these is true:

  • the option is off
  • freezed_json_serialization: false
  • the model is a union
  • the model has no optional nullable properties

Changes:

  • SWPConfig / GeneratorConfig / FillController / ProgrammingLanguage: read freezed_nullable_presence from YAML (inheriting from the root config) and pass it to the Freezed template, the same way freezed_json_serialization (using freezed required json_serializable #473) is wired.
  • dart_freezed_dto_template.dart: presence-aware factory parameters, fromJson/toJson and helper class. The existing @JsonKey logic moved unchanged into _jsonKeyParams, so the helper class can reuse it.
  • README.md: documents the option.
  • CHANGELOG.md: adds a 1.46.0 entry. pubspec.yaml is left for the release commit, as in Fix properties becoming required when a sibling uses allOf #475.
  • Tests:
    • swp_config_test.dart: default value and YAML parsing.
    • data_classes_test.dart: expected output for a model with an optional nullable allOf reference, a nullable string and a default field. It also checks that nothing changes when the option is off or freezed_json_serialization is false.
    • New patch_optional_nullable e2e test with a PATCH endpoint.

With the option off, generation is byte-identical to main, and every existing e2e expectation passes unchanged. The parser is not touched.

Known trade-offs, open to discussion:

  • Flags on all optional properties: the parser treats every optional property as nullable, so properties that are optional but not nullable: true also get a flag. The flag is harmless, because it defaults to false and the field is omitted as before.
  • copyWith: copyWith(notes: null) does not set notesPresent. To clear a field, set the flag as well: copyWith(notes: null, notesPresent: true).
  • Name collision: a schema with both notes and a real notesPresent property would collide. I kept the name short and readable, but I can add a collision check or a configurable suffix if you prefer.

dart format, dart analyze and the full dart test suite (437 passed, 2 skipped) pass. I also enabled the option in a Flutter app with about 500 presence-tracked fields: build_runner generates everything without errors, and the app's test suite passes.

🤖 Generated with Claude Code

Winget-Software and others added 2 commits October 30, 2026 19:21
…mantics

Optional nullable fields generated as `T? field` with
`@JsonKey(includeIfNull: false)` cannot express "send an explicit null",
so a PATCH body cannot clear a field: omitted and null collapse into the
same Dart value.

With `freezed_nullable_presence: true` (default false, freezed only,
requires freezed_json_serialization), each optional nullable field gets a
`<name>Present` flag:

- fromJson sets the flag from `json.containsKey(key)`, so round trips keep
  the difference between an omitted key and an explicit null.
- toJson writes `"key": null` when the value is null and the flag is set,
  and omits the key when it is not.

Freezed JSON generation is disabled for these classes
(`@Freezed(fromJson: false, toJson: false)`) and a private
`@JsonSerializable()` helper keeps json_serializable doing all value
conversion. Unions and classes without optional nullable fields are
unchanged, and output is byte-for-byte identical when the option is off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Carapacik
Carapacik self-requested a review October 31, 2026 01:24

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Distinguish an omitted key from an explicit null in Freezed models (PATCH semantics)

1 participant