Skip to content

Model json.Unmarshal and xml.Unmarshal as guaranteeing non-nil output parameter#399

Open
nickita-khylkouski wants to merge 1 commit intouber-go:mainfrom
nickita-khylkouski:fix-342-clean
Open

Model json.Unmarshal and xml.Unmarshal as guaranteeing non-nil output parameter#399
nickita-khylkouski wants to merge 1 commit intouber-go:mainfrom
nickita-khylkouski:fix-342-clean

Conversation

@nickita-khylkouski
Copy link
Copy Markdown
Contributor

Fixes #342

Problem

A common pattern when using json.Unmarshal with a nil map/slice was being flagged as a false positive:

var outMap map[string]any
if err := json.Unmarshal([]byte("{}"), &outMap); err != nil {
    return
}
outMap["hello"] = "Hello"  // Previously flagged as error

When json.Unmarshal returns a nil error, the output parameter is guaranteed to be non-nil because the function allocates maps/slices when they're nil. This contract was not being modeled by NilAway.

Solution

Following the pattern of errors.As, we now model this contract using the ReplaceConditional hook system. The call expression is replaced with:

json.Unmarshal(data, &v) && v != nil

This adds an implicit nil check that makes NilAway understand the output parameter is non-nil after a successful unmarshal (error == nil).

The same pattern applies to xml.Unmarshal, which has identical behavior.

Changes

  • Added jsonUnmarshalAction function in hook/replace_conditional.go
  • Updated ReplaceConditional map to handle encoding/json.Unmarshal and encoding/xml.Unmarshal
  • Both functions now guarantee non-nil output parameter when error is nil
  • Added test cases in testdata/src/go.uber.org/trustedfunc/json_unmarshal.go

Testing

  • ✅ All linting checks pass
  • ✅ Full test suite passes
  • ✅ New test cases verify the fix works correctly

Alternative Approaches Considered

  1. Custom RichCheckEffect: Too complex for this use case, would require deep knowledge of the system
  2. Split blocks on Unmarshal: Incorrect approach - this is for assertion-style functions, not conditional contracts
  3. Modify assume_return.go: Not suitable - the contract is conditional (only when error is nil), not absolute

The ReplaceConditional approach is the simplest and most appropriate solution, following the established pattern from errors.As.

… parameter on nil error

Fixes uber-go#342

The json.Unmarshal and xml.Unmarshal functions allocate maps/slices when the output parameter is nil.
Following the pattern of errors.As, we now model this contract by replacing the call expression with:
    json.Unmarshal(data, &v) && v != nil

This adds an implicit nil check that makes NilAway understand the output parameter is non-nil
after a successful unmarshal (error == nil), eliminating a common false positive.
@yuxincs
Copy link
Copy Markdown
Contributor

yuxincs commented Feb 13, 2026

Thanks for the contributions!

This doesn't seem to be correct, errors.As returns a bool such that we can replace it with errors.As && target != nil. These are equivalent.

However, here the json.Marshal or xml.Marshal actually returns an error, and such replacement does not really make sense.

We should actually replace the error check err != nil with err != nil && v ! = nil. This could be a bit tricky since the error check usually wouldn't be at the same node.

With this PR, we would basically always consider v to be non-nil, right? Even if err == nil.

Perhaps I missed something, happy to discuss further.

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.

Common pattern for json.Unmarshall with nil map flagged as false positive

2 participants