Skip to content

encoding/json: Marshal now honors MarshalText for string-kind map keys #81355

Open

Description

@rkolp

Go version

go version go1.27.1 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN='/Users/foo/go/bin'
GOCACHE='/Users/foo/Library/Caches/go-build'
GOCACHEPROG=''
GODEBUG=''
GOENV='/Users/foo/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/16/kzw60w_j0d51pqty3l1bywhm0000gp/T/go-build2885541076=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='/Users/foo/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPACKAGESDRIVER=''
GOPATH='/Users/foo/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/foo/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.27.1.darwin-arm64'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='/Users/foo/Library/Application Support/go/telemetry'
GOTMPDIR=''
GOTOOLCHAIN='go1.27.1+auto'
GOTOOLDIR='/Users/foo/go/pkg/mod/golang.org/toolchain@v0.0.1-go1.27.1.darwin-arm64/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.27.1'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

json.Marshal calls MarshalText on map keys now even when the key's underlying type is a string, which doesn't match what the docs say and doesn't match 1.26.

Playground: https://go.dev/play/p/PB6ZpeHcnuE

Flip between go versions in the playground above

The doc comment on Marshal (https://pkg.go.dev/encoding/json#Marshal) says map keys of any string type are used directly, and that TextMarshaler only applies otherwise:

  Map values encode as JSON objects. The map's key type must either be a
  string, an integer type, or implement encoding.TextMarshaler. The map keys
  are sorted and used as JSON object keys by applying the following rules:
    - keys of any string type are used directly
    - keys that implement encoding.TextMarshaler are marshaled
    - integer keys are converted to strings

Guessing this is a side effect of the json package being reimplemented on top of json/v2 internally, but if it's intentional the doc comment needs to be updated, since it currently describes behavior that isn't true anymore.

What did you see happen?

Different marshaling of keys depending on golang version 1.26 vs 1.27

What did you expect to see?

Consistent marshaling between the two versions

Activity

  1. prattmic commented on Sep 4, 2026

    @prattmic
    Member
  2. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Sep 4, 2026
  3. dsnet commented on Sep 4, 2026

    @dsnet
    Member

    Thank you for the report. Wow... So MarshalText is NOT used, but UnmarshalText IS used. Surprisingly inconsistency.

    Yes, this is a result of switching to the v2 implementation, which consistently calls method implementations. We'll fix this regression.

  4. gopherbot commented on Sep 4, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/827864 mentions this issue: encoding/json: avoid calling MarshalText on named string map keys

  5. dsnet commented on Sep 4, 2026

    @dsnet
    Member

    @gopherbot Please backport to 1.27. This is a regression where MarshalText is called on map keys of string kind, when it previously it did not in Go 1.26.

  6. gopherbot commented on Sep 4, 2026

    @gopherbot
    Contributor

    Backport issue(s) opened: #81361 (for 1.26), #81362 (for 1.27).

    Remember to create the cherry-pick CL(s) as soon as the patch is submitted to master, according to https://go.dev/wiki/MinorReleases.

  7. added a commit that references this issue on Sep 4, 2026
    714d9af
  8. added this to the Go1.28 milestone on Sep 8, 2026
  9. added
    NeedsFixThe path to resolution is known, but the work has not been done.
    and removed
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Sep 8, 2026
  10. gopherbot commented on Sep 15, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/832764 mentions this issue: [release-branch.go1.27] encoding/json: avoid calling MarshalText on named string map keys

  11. 0xff-dev commented on Oct 3, 2026

    @0xff-dev
    Contributor

    The fix for this issue (CL https://go.dev/cl/827864) also stopped calling MarshalText on map keys of pointer or interface kind that refer to a string kind, which differs from Go 1.26:

    type Color string
    
    func (c Color) MarshalText() ([]byte, error) {
          return []byte("C_" + string(c)), nil
    }
    
    red := Color("red")
    json.Marshal(map[*Color]int{&red: 1})                   // Go 1.26: {"C_red":1}, tip: {"red":1}
    json.Marshal(map[encoding.TextMarshaler]int{red: 1})    // Go 1.26: {"C_red":1}, tip: {"red":1}

    Since CL 827864 was backported to Go 1.27, this would ship in go1.27.2 as well.

    Fix: https://go.dev/cl/843805

  12. gopherbot commented on Oct 3, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/843805 mentions this issue: encoding/json: call MarshalText on map keys not of string kind

  13. dsnet commented on Oct 9, 2026

    @dsnet
    Member

    @gopherbot backport CL/843805 to Go 1.27 as it fixes a regression with how MarshalText is handled on a pointer receiver type used as a map key.

  14. reopened this on Oct 9, 2026
  15. jtorvald commented on Oct 28, 2026

    @jtorvald

    The second example in #81355 (comment) is still broken on tip after CL 843805. Only the pointer case was fixed:

    type Color string
    
    func (c Color) MarshalText() ([]byte, error) {
    	return []byte("C_" + string(c)), nil
    }
    
    red := Color("red")
    json.Marshal(map[*Color]int{&red: 1})                 // Go 1.26 and tip: {"C_red":1}
    json.Marshal(map[encoding.TextMarshaler]int{red: 1})  // Go 1.26: {"C_red":1}, tip: {"red":1}

    The test added in CL 843805 only puts pointers into the interface, never a string-kind value. The interface marshaler copies the value into a new addressable value, so the method marshaler treats it like a string-kind map key and skips MarshalText. Errors returned by MarshalText are also silently dropped in this case.

    Fix: https://go.dev/cl/855441. It decides "use the string as-is" in the map marshaler based on the map's key type (as Go 1.26 did), instead of in the method marshalers.

  16. gopherbot commented on Oct 28, 2026

    @gopherbot
    Contributor

    Change https://go.dev/cl/855441 mentions this issue: encoding/json: call MarshalText on string-kind map keys in an interface

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsFixThe path to resolution is known, but the work has not been done.

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions