Skip to content

proposal: encoding/json/v2: custom unmarshalers for underlying types should work with named types #75177

@flyhope

Description

@flyhope

Proposal Details

Summary

Custom unmarshalers registered for underlying types (e.g., int) should also apply to named types based on those underlying types (e.g., type Num int). Currently, the custom unmarshaler is ignored when the target is a named type.

Go Version

go version go1.25 linux/amd64

What did you do?

I registered a custom unmarshaler for the int type and expected it to also work when unmarshaling into a named type Num that has int as its underlying type.

package main

import (
    "encoding/json/jsontext"
    "encoding/json/v2"
    "fmt"
)

type Num int

func main() {
    var value Num
    opt := json.WithUnmarshalers(json.UnmarshalFromFunc(func(decoder *jsontext.Decoder, t *int) error {
        if err := decoder.SkipValue(); err != nil {
            return err
        }
        *t = 2
        return nil
    }))

    if err := json.Unmarshal([]byte(`1`), &value, opt); err != nil {
        panic(err)
    }

    fmt.Println(value) // Expected: 2, Actual: 1
}

What did you expect to see?

Expected output: 2

The custom unmarshaler should be called because Num has int as its underlying type.

What did you see instead?

Actual output: 1

The custom unmarshaler is not called, and the default JSON unmarshaling behavior is used.

Why this matters

This limitation forces developers to:

  1. Enumerate all named types: Register separate unmarshalers for each named type, even when they share the same underlying type and logic.
// Current workaround - very verbose
opt := json.WithUnmarshalers(
    json.UnmarshalFromFunc(customIntUnmarshaler[int]),
    json.UnmarshalFromFunc(customIntUnmarshaler[Num]),
    json.UnmarshalFromFunc(customIntUnmarshaler[UserID]),
    json.UnmarshalFromFunc(customIntUnmarshaler[ProductID]),
    // ... many more named int types
)
  1. Implement interfaces for every type: Add UnmarshalJSONV2 method to each named type, duplicating logic.
func (n *Num) UnmarshalJSONV2(dec *jsontext.Decoder, options json.UnmarshalOptions) error {
    return customIntLogic(dec, (*int)(n))
}
func (uid *UserID) UnmarshalJSONV2(dec *jsontext.Decoder, options json.UnmarshalOptions) error {
    return customIntLogic(dec, (*int)(uid))
}
// ... repeat for every named type

Proposed Solution

The JSON v2 package should check for custom unmarshalers of the underlying type when no specific unmarshaler is found for a named type. The lookup order should be:

  1. Exact type match (current behavior)
  2. Underlying type match (proposed enhancement)
  3. Default behavior (current fallback)

This would align with Go's type system where named types can be converted to their underlying types.

Alternative Considered

While implementing UnmarshalJSONV2 on each type works, it creates maintenance burden and code duplication, especially in codebases with many similar named types (IDs, codes, measurements, etc.).

Benefits

  1. Reduced boilerplate: One unmarshaler can handle multiple related types
  2. Better maintainability: Changes to parsing logic only need to be made in one place
  3. Consistency with Go's type system: Named types are convertible to their underlying types
  4. Backward compatibility: Existing code continues to work unchanged

This enhancement would make JSON v2 more developer-friendly while maintaining type safety and performance.

Metadata

Metadata

Assignees

No one assigned

    Labels

    LibraryProposalIssues describing a requested change to the Go standard library or x/ libraries, but not to a toolProposal

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions