Skip to content

fix: omit the agent "for" key when no party is named - #13

Merged
momilo merged 3 commits into
mainfrom
fix/agent-for-omitzero
Aug 31, 2026
Merged

momilo merged 3 commits into
mainfrom
fix/agent-for-omitzero

Conversation

@momilo

@momilo momilo commented Aug 30, 2026 •

Copy link
Copy Markdown
Collaborator

An agent with no for party goes on the wire as "for": null, and a receiver decodes that as an agent whose owner is the empty string.

Serialisation

Agent.For is a ForField struct tagged json:"for,omitempty". omitempty has no effect on a struct type, so the field is always emitted, and ForField.MarshalJSON returns json.Marshal(nil) when it holds no values. Every ownerless agent — a settlement address with no owner, say — therefore carries "for": null.

Deserialisation

encoding/json documents unmarshalling null into a non-pointer as a no-op that returns no error. ForField.UnmarshalJSON tries json.Unmarshal(data, &single) first, so null takes that branch with single == "", leaving values = []string{""} and IsEmpty() == false.

So a receiver concludes the agent declares an owner, whose DID is the empty string, when the sender said it has none. TAIP-5 makes for optional on an agent, so the correct encoding of "no party named" is an absent key.

Changes

  • json:"for,omitzero" on Agent.For. omitzero applies to structs, which is the thing omitempty cannot do. Plus an IsZero method, so a ForField built from an empty non-nil slice omits the key as well as the nil-slice zero value.
  • ForField.UnmarshalJSON handles null explicitly before the single-string branch. This half is not needed once senders stop emitting the key, but it protects receivers against senders that have not upgraded.

ConfirmRelationship.For is untouched — TAIP-9 requires that field, and it is tagged json:"for" with no omission.

Tests

Four cases in types_test.go, each verified to fail without the change:

  • an ownerless Agent marshals without a for key, and round-trips back to IsEmpty();
  • a ForField built from an empty slice omits the key;
  • null decodes to no values rather than [""];
  • an Agent whose JSON carries "for": null decodes to no owner.

Module is already on Go 1.26, so omitzero needs no toolchain bump.

Second commit: go 1.26.6

govulncheck reports nine standard-library vulnerabilities against the toolchain the go directive selects, and CI resolves its Go version from go-version-file: go.mod — so the Vulncheck job has been failing on main since July, independently of this change. All nine are fixed by 1.26.6, the highest "fixed in" across the set, and none originate in a dependency, so the directive is the only thing that needs to move. Clean locally afterwards.

They are reachable from this module rather than theoretical — the traces run through Client.ReceiveUnverified into url.URL.Parse, tls.Conn.HandshakeContext and asn1.Unmarshal.

It is a patch-level bump within the same minor, so consumers need a 1.26.6 toolchain rather than a new language version. Happy to split it into its own PR if you'd rather keep this one to the for field.

`omitempty` has no effect on a struct field, so `Agent.For` was always
serialised. A zero ForField marshals to `null`, which put `"for": null` on the
wire for every ownerless agent — a settlement address with no owner, for
instance.

Decoding that is worse than emitting it. `encoding/json` documents
unmarshalling `null` into a non-pointer as a no-op that returns no error, so
ForField.UnmarshalJSON's single-string branch accepted it, leaving
`values = [""]` and `IsEmpty() == false`. A receiver reads an agent that
declares an owner whose DID is the empty string, when the sender said the agent
has none.

Two changes:

- `json:"for,omitzero"` on Agent.For, plus an IsZero method so a ForField built
  from an empty non-nil slice omits the key too. omitzero applies to structs,
  which is what omitempty could never do.
- UnmarshalJSON rejects `null` up front, so a receiver that has not upgraded
  past a sender still emitting `"for": null` decodes it as no owner.

TAIP-5 makes `for` optional on an agent, so an absent key is the correct
encoding of "no party named".
@momilo
momilo marked this pull request as draft August 30, 2026 22:18
govulncheck reports nine standard-library vulnerabilities against the toolchain
the go directive selects, and CI builds with go-version-file: go.mod, so the
whole matrix has been red on main since July. Every one of them is fixed by
1.26.6 — the highest "fixed in" across the set — and none come from a
dependency, so the directive is the only thing that needs to move.

Reachable from this module rather than theoretical: the traces run through
Client.ReceiveUnverified into url.URL.Parse, tls.Conn.HandshakeContext and
asn1.Unmarshal.

A patch-level bump within the same minor, so consumers need a 1.26.6 toolchain
rather than a new language version.
@momilo
momilo marked this pull request as ready for review August 31, 2026 16:58
@momilo
momilo merged commit 6fe6fa3 into main Aug 31, 2026
3 checks passed
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.

1 participant