Skip to content

Decide dominance on the fields encoding/json sees - #129

Merged
FumingPower3925 merged 1 commit into
mainfrom
fix-phantom-properties
Aug 27, 2026
Merged

FumingPower3925 merged 1 commit into
mainfrom
fix-phantom-properties

Conversation

@FumingPower3925

Copy link
Copy Markdown
Owner

Closes #127.

Three struct shapes documented a property name the wire never carried. Each is quiet: the document built, validated and rendered, and only a consumer diffing it against real traffic would have noticed.

An unexported struct embedded under json:"-" grew a property literally named - holding the nested object. inspectField routed unexported fields to unexportedEmbed before the tag check an exported field would have hit, so the skip never ran. That check moves above the branch, and stays limited to the bare tag: json:"-," names the key -, and encoding/json writes the embedded object under it whether or not the embedding is exported.

A chan, func or unsafe.Pointer field sharing a json: tag with an ordinary field left that rival documented on its own. encoding/json keeps the unmarshalable field while it resolves names, so the two conflict and neither is written; stdocs removed it in inspectField, before dominance ran, and documented the key the conflict takes away. Such a field now comes back as an excluded candidate: it competes for the name and is filtered out once dominance has settled. The openapi: tag panic for a field with no JSON form moves along with it.

A third shape, not in the issue. The same held one level up: a struct embedded under openapi:"-" was cut before its fields could collide with a name outside it, so a rival it hides came out documented unopposed. The walk now queues each type with an excluded flag, so a hidden subtree is still walked for names and its candidates travel excluded.

Testing

TestDroppedFieldsMatchWire pins all three against a live json.Marshal, in the differential style the issue suggested. Two of its types are assembled with reflect.StructOf, because go vet rejects a literal struct that promotes one json tag twice and no nolint directive quiets it.

Each fix was negative-controlled: reverting it individually makes its case fail (the third with schema documents "dup" but the wire never carries it) and leaves the others passing.

Full suite green under -race on both go1.26.6 and go1.27.0 (15 packages plus the YAML round-trip module), gofmt/go vet clean, golangci-lint 0 issues, and 25s of FuzzReflectSchema on 1.27 with no new failures.

Three struct shapes documented a property name the wire never carried.
Each is quiet: the document built, validated and rendered, and only a
consumer diffing it against real traffic would have noticed.

An unexported struct embedded under json:"-" grew a property literally
named - holding the nested object. inspectField routed unexported
fields to unexportedEmbed before the tag check an exported field would
have hit, so the skip never ran. That check moves above the branch, and
stays limited to the bare tag: json:"-," names the key -, and
encoding/json writes the embedded object under it whether or not the
embedding is exported.

A chan, func or unsafe.Pointer field sharing a json: tag with an
ordinary field left that rival documented on its own. encoding/json
keeps the unmarshalable field while it resolves names, so the two
conflict and neither is written; stdocs removed it in inspectField,
before dominance ran, and documented the key the conflict takes away.
Such a field now comes back as an excluded candidate: it competes for
the name and is filtered out once dominance has settled. The openapi:
tag panic for a field with no JSON form moves along with it.

The same held one level up, which the issue does not cover. A struct
embedded under openapi:"-" was cut before its fields could collide with
a name outside it, so a rival it hides came out documented unopposed.
The walk now queues each type with an excluded flag, so a hidden
subtree is still walked for names and its candidates travel excluded.

TestDroppedFieldsMatchWire pins all three against a live marshal. Two
of its types are assembled with reflect.StructOf, because go vet
rejects a literal struct that promotes one json tag twice and no nolint
directive quiets it.
@FumingPower3925
FumingPower3925 merged commit e08c987 into main Aug 27, 2026
45 checks passed
@FumingPower3925
FumingPower3925 deleted the fix-phantom-properties branch August 27, 2026 08:27
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.

Two struct shapes document a key encoding/json never writes

1 participant