Skip to content

Bump required Go version to 1.27 and implement json/v2.UnmarshalerFrom and json/v2.MarshalerTo - #16

Merged
jonathansharman merged 6 commits into
mainfrom
maintenance/go-1.27
Sep 9, 2026
Merged

jonathansharman merged 6 commits into
mainfrom
maintenance/go-1.27

Conversation

@jonathansharman

@jonathansharman jonathansharman commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor

Documentation

Addresses this bug report

Description

This bumps the Go version to 1.27, updates dependencies, and adds UnmarshalJSONFrom and MarshalJSONTo methods to both Update and SliceUpdate.

This also runs go fix to modernize the module and switches the two UnmarshalJSON tests to be closure-driven.

Testing Considerations

Tested in https://github.com/nicheinc/apply/pull/113

Versioning

Minor.

@jonathansharman jonathansharman added the ai-usage:3-collaborative Authored collaboratively by a human and AI agent, with direct code contributions from both label Sep 9, 2026
Comment thread nup/sliceUpdate_test.go
Comment on lines +86 to +87
var update SliceUpdate[int]
err := update.UnmarshalJSON([]byte(testCase.json))

@jonathansharman jonathansharman Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling json.Unmarshal will just call UnmarshalJSONFrom since it has higher precedence. Calling Update.UnmarshalJSON directly is the simplest way to exercise the function.

Comment thread nup/update.go
// MarshalJSONTo implements [json.MarshalerTo].
func (u Update[T]) MarshalJSONTo(encoder *jsontext.Encoder) error {
if u.op == OpSet {
return json.MarshalEncode(encoder, u.value)

@jonathansharman jonathansharman Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using the json/v2 functions in MarshalJSONTo/UnmarshalJSONFrom entails a potential change of behavior when calling json.Marshal/Unmarshal, depending on T. For primitives, the behavior will be unchanged, but if for instance T is a struct that uses omitempty on an int field, that field will no longer be omitted when 0. (The struct would need to switch to omitzero to maintain its old behavior.)

However, adding new methods to a struct is not considered a breaking change, even though in this case it can change the behavior of existing code. Pragmatically, nup updates are usually used for primitives (or slices of primitives), not complex structs.

@jonathansharman
jonathansharman marked this pull request as ready for review September 9, 2026 03:14
Comment thread nup/update.go Resolved
Comment thread nup/sliceUpdate.go Resolved
Comment thread go.mod Resolved
Comment thread nup/update.go Resolved
@jonathansharman

jonathansharman commented Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Code review

Reviewed against the PR description and Niche's Go backend conventions. Four inline comments posted; this comment covers whether the change matches what it says it does.

No Jira ticket key appears in the PR title, description, or the maintenance/go-1.27 branch name — the linked reference is a Slack thread — so there were no acceptance criteria to check the diff against. Everything below is from comparing the diff to the PR description itself.

Verified, and deliberately not flagged

Recording these so they don't get re-litigated on the next review pass:

  • json:"," → json:"" in the test fixtures is correct and necessary, not a go fix artifact to be guarded against like the sibling NeverEmpty tag. json/v2 rejects that tag outright, with Go struct field EmptyName has malformed json tag: invalid trailing ',' character. Coverage doesn't suffer either — getKeyName is still at 100%, since the ",omitempty" case exercises the same empty-name-before-comma branch.
  • v1 compatibility options are inherited by the nested json.UnmarshalDecode / json.MarshalEncode calls. Confirmed by round-tripping a struct with mismatched field-name casing through encoding/json v1: the value inside an Update matched case-insensitively exactly like a directly-embedded struct, so v1 callers don't silently pick up v2 field-matching semantics for the wrapped type.
  • omitzero already produces byte-identical output to the deprecated nup.MarshalJSON helper for a mixed noop/remove/set struct, so the existing docs steering users to omitzero remain accurate under json/v2.
  • The omitempty-on-a-struct-T behavior change is already documented in your own inline note, and go vet, gofumpt, and the full test suite (197 tests, 100% statement coverage) are clean.

🤖 Comment from code-review-niche Claude Code agent 🤖

Comment thread nup/update.go
// UnmarshalJSON implements [jsonv1.Unmarshaler].
func (u *Update[T]) UnmarshalJSON(data []byte) error {
if string(data) == "null" {
u.op = OpRemove

@jonathansharman jonathansharman Sep 9, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Claude review pointed out that unmarshaling null into a destination update with a non-nil value would result in an update whose operation is OpRemove but which would not compare equal with Remove[T](). I've updated all four unmarshal implementations to fully overwrite the destination on the non-error paths and never overwrite (even partially) on an error path.

This is once again a small change in functionality for struct types, which will no longer be merged into, but (1) we almost never unmarshal into a non-zero value, (2) we almost never use non-primitive update fields, and (3) the old merge semantics were not my intention, so I'd call this a bug fix.

@jonathansharman jonathansharman changed the title Bump required Go version to 1.27 and implement json/v2 marshal/unmarshal interfaces Bump required Go version to 1.27 and implement json/v2.UnmarshalerFrom Sep 9, 2026
@jonathansharman jonathansharman changed the title Bump required Go version to 1.27 and implement json/v2.UnmarshalerFrom Bump required Go version to 1.27 and implement json/v2.UnmarshalerFrom and json/v2.MarshalerTo Sep 9, 2026

@DavidBrown-niche DavidBrown-niche left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the fix.

I agree that the better fix is to just switch to requests.JSONBody, fortunately we don't seem to have too many structs that have custom marshalling/unmarshalling and nup is by far the most commonly used one that does, so this seems like a fine solution to preserve existing behavior without needing larger changes.

@jonathansharman
jonathansharman merged commit e74ac19 into main Sep 9, 2026
7 checks passed
@jonathansharman
jonathansharman deleted the maintenance/go-1.27 branch September 9, 2026 12:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-usage:3-collaborative Authored collaboratively by a human and AI agent, with direct code contributions from both

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants