No JSON null in API and events¶
Context and Problem Statement¶
Many fields are optional: a column with NULL, an event without an actor, a run without a session.
JSON has two forms for this: the field is missing, or it is present with null. All clients are
generated from one specification (ADR 0013): Go server, TypeScript, Python and Dart. The
Dart generator dart-dio does not support the null type [Q1, verified]. How do the API, the
gateway and webhooks express "no value", and how does a client remove a value?
Decision Drivers¶
- All four generators produce working code from the same specification (gate G4 in 06-api.md).
- One form for "no value" in responses, requests and events.
- No three-valued logic in changes (missing,
null, value) that every client must interpret correctly. - Conservative schemas: no type array, no
anyOf(06, section 14). - Unknown and unexpected input is tolerated and does not lead to a crash (06, section 2).
Considered Options¶
- No JSON
nullon the wire; a missing field means "no value"; removal through separate fields or modes nullas an explicit value with the OpenAPI 3.1 type[T, "null"]and changes following JSON Merge Patch (nullmeans delete)- Placeholder values for "no value" (empty string, 0, epoch time)
- Wrapper objects for optional fields (
{"value": ...})
Decision Outcome¶
Chosen option: "No JSON null; missing means no value", because it works with all generators
and makes changes unambiguous. Normative in
04-sync-protocol.md, section 2.2 (R-SY-16),
03-data-model.md, principle 13, and
06-api.md, section 2 (R-API-03).
- The server never sends JSON
null, neither in REST responses nor in gateway frames, envelopes, payloads or webhooks. A column with NULL appears as a missing field. - Required fields are always present, also with zero values such as
0,falseor"". Only optional fields may be missing. - Values are removed through separate fields or modes, never through
null: for exampleuse_global_order: trueremoves the own order in the sidebar, andOperatorSet.mode = nonesets "no operator". In changes a missing field means "unchanged". - Requests with
nullfail the schema validation before the handler (SEC-IN-1 in 19-security.md), because no schema allows the null type. Adapters and apps omit a field instead of sendingnull. - Clients treat a received
nulllike a missing field (tolerance, 06 section 2).
Consequences¶
- Good, because the types in Dart, TypeScript, Python and Go are generated without special cases.
- Good, because a change knows exactly two cases (field present: set; field missing: leave) and removal is visible and auditable.
- Bad, because every removable field needs its own mode or its own field; the API becomes somewhat wider at these places.
- Bad, because Go code must take care: with
omitempty, zero values of required fields disappear too. Therefore only optional fields carryomitempty(as pointers or withomitzero), required fields never; the response validation (G8) finds missing required fields. - Neutral, because the database keeps using NULL; the boundary lies in the encoding to the outside.
Confirmation¶
- Lint rule
rule/no-type-arrays-or-nullindocs/api/redocly.yaml(gate G1): no schema with a null type or a type array. - Property test in build ticket M1-18:
TestNoNull_PropertyRandomValuesandFuzzSanitizeJSONinservices/server/internal/platform/httpserver(the response writer removesnullmembers and rejectsnullelements and integers above 2^53 - 1 with500;TestNoNull_NullInArrayGives500,TestInt53_AboveLimitGives500,TestNoNull_CleanBodyByteIdentical), plus the harnesshttpservertest, which fails every handler test withnullin the response; a test in build ticket M1-16: no envelope contains JSONnull(built:FuzzEnvelopeHasNoJSONNullandTestEnvelope_FieldsMatchOpenAPIinservices/server/internal/sync; the appender rejects a payload with JSONnullat any depth,TestAppend_Validation). - Gate G7 (Schemathesis) with its own check "no
null" and gate G8 (response validation in integration tests) in 06-api.md, section 15. - Tolerance test G5: the hand-written layer of every SDK skips a received
nullwithout an error. - Test: a request with
nullin an optional field returns400withvalidation_failed.
Pros and Cons of the Options¶
No null, missing means no value¶
- Good, because all generators can represent it and Go handles it directly with
omitemptyoromitzero. - Bad, because "remove a value" is not generic but is designed per field.
null with JSON Merge Patch¶
- Good, because Merge Patch has a well-known rule:
nullremoves a value [Q2, verified]. - Bad, because
dart-diodoes not support the null type in the schema [Q1, verified]; the optionuseOptionaldoes distinguish missing,nulland value [Q1, verified], but helps only in the Dart client. Type arrays are supported to different degrees in the generators (oapi-codegen only since v2.8.0, 06 source Q1). - Bad, because every client must correctly distinguish three states (missing,
null, value); a client that treatsnulland "missing" the same deletes or keeps values unintentionally.
Placeholder values¶
- Good, because every field is always present.
- Bad, because
0,falseand""are often real values (counters, switches); placeholders confuse "no value" with a valid value.
Wrapper objects¶
- Good, because unambiguous.
- Bad, because every optional field becomes an object; schemas, examples and generated code become considerably larger without any generator requiring it.
More Information¶
- Sources (retrieved 2026-10-10): Q1 openapi-generator
dart-dio, https://openapi-generator.tech/docs/generators/dart-dio/ [verified]; Q2 RFC 7396 JSON Merge Patch, section 1, https://www.rfc-editor.org/rfc/rfc7396 [verified]. omitzeroexists inencoding/jsonsince Go 1.24 (https://go.dev/doc/go1.24, retrieved 2026-10-10) [verified].- Related: ADR 0013 (spec-first), ADR 0015 (error format), ADR 0026 (SDKs), ADR 0052 (envelope).