feat(JSON): add skip_null_fields option to omit null-valued record fields#44
Open
ggreif wants to merge 2 commits intoNatLabs:mainfrom
Open
feat(JSON): add skip_null_fields option to omit null-valued record fields#44ggreif wants to merge 2 commits intoNatLabs:mainfrom
ggreif wants to merge 2 commits intoNatLabs:mainfrom
Conversation
…elds External HTTP APIs frequently reject payloads that carry optional fields as explicit null (e.g. Twitter's /2/tweets rejects 16 such entries in a single request). Motoko records serialised through `to_candid |> JSON.toText` always emit every field, including `?T = null`, which hits exactly that rejection case for any OpenAPI-style client. This commit adds a new `skip_null_fields : Bool` option to `CandidType.Options`. When `true`, the JSON encoder omits entries in `#Record`/`#Map` whose value resolves to `#Null`. Default is `false` — all existing behaviour is preserved. Also exposes `fromCandidWith` so callers working with an already- decoded Candid value can request the same behaviour without going through `toText`. Test: `tests/JSON.Test.mo` verifies (a) default keeps the nulls, (b) flag drops them, (c) non-null optionals still survive. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Complements the JSON encoder's treatment of the new `Options.skip_null_fields` flag. Both CBOR and UrlEncoded walk `#Record`/`#Map` unconditionally today, emitting CBOR null and `key=null` respectively for `?T = null` optional fields; those land in outbound HTTP bodies and trip the same class of strict type-validator rejections (e.g. Twitter /2/tweets). * CBOR `transpile_candid_to_cbor`: skip record entries whose value encodes to `#majorType7(#_null)` when `options.skip_null_fields` is true. * UrlEncoded: introduce `fromCandidWith(candid, skip_null_fields)` (keeping `fromCandid` as the default-behaviour wrapper), thread the flag through `toKeyValuePairs`, and elide the `key=null` pair at the `#Null` leaf. Tests added in tests/CBOR.Test.mo and tests/UrlEncoded.Test.mo verify both default (keep nulls) and opt-in (omit) paths; 10/10 test files pass. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
Extended the patch to CBOR and UrlEncoded as well — both had the same unconditional record/map walk that emits null-valued optional fields (CBOR as
Tests added in Commit: a63d60b. |
ggreif
added a commit
to caffeinelabs/x-client
that referenced
this pull request
Apr 20, 2026
…dies Twitter's /2/tweets (and other strict X endpoints) reject payloads that carry optional fields as explicit null. The generated toJSON always emitted every ?T field, which made valid-looking Motoko calls unable to produce accepted requests. This regen swaps the JSON serialisation dependency from serde@3.5.0 to serde-core@0.1.0 (a fork carrying NatLabs/serde#44) and threads `?{ Candid.defaultOptions with skip_null_fields = true }` through every JSON.toText call in the API layer. Null-valued entries are now omitted from outbound JSON, matching how external schemas read "field absent". Public Motoko API of this client is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ggreif
added a commit
to ggreif/openapi-generator
that referenced
this pull request
Apr 20, 2026
- modules/.../motoko/mops.toml.mustache: swap serde@3.5.0 for serde-core@0.1.0, a ggreif/serde fork carrying the `skip_null_fields` JSON option pending NatLabs/serde#44. - modules/.../motoko/api.mustache: import `JSON` and `Candid` from `mo:serde-core`; pass `?{ Candid.defaultOptions with skip_null_fields = true }` as the options argument to every `JSON.toText` call. Null-valued optional fields no longer appear in outbound JSON, unblocking strict-validation endpoints such as Twitter's `/2/tweets` (16 fields were being rejected there). - bin/configs/motoko-x.yaml: bump to 0.1.2. - samples/client/x/motoko/generate.sh: align prologue with OpenAI's style (set -euo pipefail, explicit SCRIPT_DIR/ REPO_ROOT, --skip-validate-spec) — the previous version had `cd ../../..` which only climbed three levels, silently failing to find the JAR. - samples/client/x/motoko/typecheck.sh: pin the tmp-build moc toolchain to 1.4.1 (was 1.3.0, pre-Float32 — `core@2.4.0` uses Float32 and failed to build). - samples/client/x/motoko/generated: submodule bump to v0.1.2 (caffeinelabs/x-client@bfd7cd3, published to mops). Public Motoko API of x-client is unchanged; this is a patch-level bugfix release. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Motivation
External HTTP APIs frequently reject request bodies that carry optional fields as explicit
null— they validate field types strictly and consider"field": nulla type mismatch. Twitter'sPOST /2/tweetsis a concrete example: it rejects 16 such entries in a single request when the generated client forwards a partly-filled payload.Motoko records serialised through
to_candid |> JSON.toTextalways emit every field, including?T = nullvalues, which hits exactly that rejection case for any OpenAPI-style client that mirrors a nullable-optional spec field as an?Ton the Motoko side. Today there is no option to suppress them in serde.What this PR does
Adds a new
skip_null_fields : Booloption toCandidType.Options:When
true, the JSON encoder omits entries in#Record/#Mapwhose value resolves to#Null.#Option(#Null)— which is what?T = nullbecomes after the candid round-trip — is the common case; deeply nested#Option(#Option(#Null))also collapses correctly.Defaults to
false, so all existing runtime behaviour is preserved.Also exposes
fromCandidWith(candid, skip_null_fields)for callers that already hold a decodedCandidvalue and want the same behaviour without going throughtoText.Adding a required field to the public
Optionsrecord is a type-level breaking change under Motoko's structural record typing. Code that constructsOptionsas a full record literal will no longer type-check:The idiomatic
{ Candid.defaultOptions with … }pattern is unaffected — it inherits the new field fromdefaultOptions = false. All of serde's own tests and (spot-checked) the main consumers I know of use that pattern and continue to compile and pass.Recommended version bump: 3.5.0 → 4.0.0. Happy to land the change under whichever bump the maintainers prefer.
Before / after
Tests
tests/JSON.Test.mogains askip_null_fields omits null optional fieldscase that verifies:{"text":"hello"}exactly.?false→false,?"everyone"→"everyone").nulltokens leak through.Full test suite:
npx ic-mops test— 10/10 files pass (9 previously-green + the new one).🤖 Generated with Claude Code