Skip to content

fix(golang): don't apply omitempty to nullable/union fields under --omit-empty#3057

Merged
schani merged 4 commits into
masterfrom
agent/fix-issue-2515
Jul 21, 2026
Merged

fix(golang): don't apply omitempty to nullable/union fields under --omit-empty#3057
schani merged 4 commits into
masterfrom
agent/fix-issue-2515

Conversation

@schani

@schani schani commented Jul 20, 2026

Copy link
Copy Markdown
Member

Bug

--omit-empty tags all optional Go struct fields with ,omitempty,
including fields whose type is nullable/union (e.g. *string for a
["string", "null"] schema type). Since Go's encoding/json treats a
nil pointer with omitempty as "absent", this conflates "field not
present" with "field explicitly null" and silently drops legitimate
null values from marshalled output.

This surfaces concretely with oneOf: when a property is required and
nullable in one branch but optional in another, unifying the branches
makes it an optional property on the merged Go struct. With
--omit-empty that property gets ,omitempty, so a document valid
against the branch that requires it as null (e.g.
{"kind":"one","b":null}) round-trips to {"kind":"one"} — no longer
valid against any branch.

Root cause

canOmitEmpty in packages/quicktype-core/src/language/Golang/utils.ts
already excludes union/null/any-typed properties from omitempty
in the default (flag-off) path, specifically because of this
absent-vs-null conflation. But the --omit-empty branch bypassed that
exclusion unconditionally:

export function canOmitEmpty(
    cp: ClassProperty,
    omitEmptyOption: boolean,
): boolean {
    if (!cp.isOptional) return false;
    if (omitEmptyOption) return true;   // <- bug: no type exclusion here
    const t = cp.type;
    return !["union", "null", "any"].includes(t.kind);
}

Fix

Apply the same union/null/any exclusion under --omit-empty:

if (omitEmptyOption)
    return !["union", "null", "any"].includes(cp.type.kind);

--omit-empty still widens omitempty to other optional property types
(its intended purpose); it now no longer overrides the deliberate
exclusion for nullable/union types.

Test coverage

  • Added test/inputs/schema/nullable-optional-one-of.schema — a oneOf
    with two variants sharing a nullable property b that's required in
    one branch and optional in the other (mirrors the issue's repro), plus
    a positive sample nullable-optional-one-of.1.json ({"kind":"one","b":null}).
    Wired into GoLanguage.quickTestRendererOptions in test/languages.ts
    with {"omit-empty": "true"} so it runs as a schema fixture test.
  • Updated the existing omit-empty JSON fixture's expected output
    (test/inputs/json/priority/omit-empty.out.omit-empty.json) to reflect
    that a nullable optional field now preserves null instead of being
    dropped by omitempty.

I verified locally, before involving the fix, that reverting it makes
the new fixture's generated Go tag regress back to
json:"b,omitempty" (reproducing the bug), confirming the test actually
exercises the fix.

Verification

  • npm run build passes.
  • Re-ran the issue's exact repro
    (node dist/index.js --src-lang schema --lang go --just-types-and-package --omit-empty def.json):
    now generates B *string \json:"b"`(noomitempty`), matching the
    expected output.
  • QUICKTEST=true FIXTURE=golang script/test — 47/47 pass.
  • QUICKTEST=true FIXTURE=schema-golang script/test — 73/73 pass
    (including the new nullable-optional-one-of.schema case, both with
    and without omit-empty).
  • npm run test:unit — 176/176 pass.
  • npx biome check on changed files — clean.
  • Confirmed --omit-empty still applies to genuinely non-nullable
    optional fields (unaffected by this change).
  • Everything else (Go toolchain compile/round-trip within the fixture
    runner, CI matrix across other languages) is left to CI.

Fixes #2515

🤖 Generated with Claude Code

schani and others added 4 commits July 20, 2026 19:17
…mit-empty (#2515)

When a oneOf branch unification makes a nullable/union-typed property
optional, --omit-empty tagged it json:"field,omitempty". Since Go's
omitempty on a pointer conflates "absent" with an explicit JSON null,
this silently dropped required null values (e.g. {"kind":"one","b":null})
from marshalled output, producing schema-invalid JSON.

canOmitEmpty already excluded union/null/any-typed properties from
omitempty in the default (flag-off) path; --omit-empty unconditionally
bypassed that exclusion. Now the exclusion applies regardless of the
flag, while --omit-empty still widens omitempty to other optional
property types as intended.

Added test/inputs/schema/nullable-optional-one-of.schema (a oneOf where
one variant requires a nullable property, the other doesn't) run with
omit-empty via GoLanguage.quickTestRendererOptions, and updated the
existing omit-empty fixture's expected output to reflect that nullable
fields now preserve null instead of being dropped.

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
…a on cjson (#2515)

The .1.fail.enum.json sample added in #3057 relies on the generated code
rejecting an invalid "kind" enum value, but cjson's enum decoder silently
falls back to the first variant instead of failing. Add the schema to the
existing skipsEnumValueValidation list, which already covers this same gap
for cjson/crystal/swift/haskell/typescript-zod/typescript-effect-schema.

Co-Authored-By: gpt-5.6-sol via pi <noreply@openai.com>
@schani
schani merged commit f1df437 into master Jul 21, 2026
28 checks passed
@schani
schani deleted the agent/fix-issue-2515 branch July 21, 2026 15:07
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.

Golang: Omitempty when field is required by some oneOf variants

1 participant