conceptadev / conceptadev/ack

Encode-side key presence for optional nullable fields

Open
#148 0 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
Dart
Stars
63
Forks
3
Avg merge
15h 12m
Merged PRs (30d)
12

Description

## Summary

Parse already distinguishes three JSON states for an optional nullable property:

| Wire | Dart after parse |
| --- | --- |
| key omitted | `null` |
| `"payload": null` | `null` |
| `"payload": ` | value |

`@Optional()` / `@Required()` / `@NotNull()` are parse-side. Unannotated `T?` is `.optional().nullable()`: missing and present-null both become Dart `null`. Generated encode then omits Dart null (`includeIfNull: false`).

`T?` is one slot. After parse, omit and present-null are the same value, so encode cannot write `"payload": null` even when the key was present.

#145 called this out as a non-goal. It is still true after #146.

## Why this is not #145 / #146

#146 infers `Object?` → `Ack.any().nullable()` and `Map` → `Ack.map(...)`. That is the type table. It does not add a presence bit.

`@NotNull()` rejects present JSON null on parse. It does not tell encode “this key was present.”

Flipping `includeIfNull` only swaps which case is lost: always emit null, or always omit. Consumers that must round-trip both still cannot.

## Current consumer workaround

A handwritten flag next to the field:

```dart
final class Envelope {
Envelope({this.payload, this.hasPayload = false});

final Object? payload;
final bool hasPayload;

Map toJson() => {
if (hasPayload) 'payload': payload,
};
}
```

`hasPayload` is not a JSON field, so the class cannot be `@AckModel`. The payload itself is ordinary JSON-any and would generate fine under #146.

## Proposed direction

An encode-side presence type, not a parse annotation.

Something like `JsonMaybe` / `JsonPresence` with three states: absent, present-null, value. Class-first infers it to `.optional().nullable()` on parse and uses the presence tag on encode.

Do not:

- flip global `includeIfNull`
- overload `@NotNull()` to mean encode presence
- treat a `bool hasX` field as a JSON key

Omit-if-empty / omit-default (`{}`, `false`, empty lists) is a separate policy. This issue is only omit vs present-null vs value.

## Acceptance

- A generated model can parse `{"payload": null}` and encode that same key back as null.
- Omitted input still encodes as omitted.
- `T?` keeps today’s collapse. Presence is opt-in via the new type (or an explicit field annotation that is not `@NotNull()`).
- #146 stays mergeable without this.

## Related

#145 (non-goal: three-way key presence / `JsonMaybe`). #146 implements the type table only.

Contributor guide

Open the contributing guide

Research direction

No implementation files or tests are named. Start by tracing the generated parse and encode paths discussed alongside #146, then compare the behavior with the stated three-state acceptance cases: present-null round-trips, omitted input stays omitted, and ordinary T? behavior remains unchanged.

Written by the indexing model from the issue text.

Assessment

Tech stack
dart
Domain
backend-api-design
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
38/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.