Skip to content

Default json Codec fails #3855

Description

@snac21

What happened:

Default json Codec fails to unmarshal string-formatted int64 fields in application/json requests

In standard Protobuf JSON mapping (proto3 specification), int64 values are allowed and recommended to be serialized/unserialized as JSON strings (e.g., "id": "123" instead of "id": 123) to prevent precision loss in 64-bit JavaScript numbers.

In Kratos v3, when web clients (e.g., Axios, Vue, React, Postman) send HTTP POST requests with Content-Type: application/json, attempting to send "id": "123" for a Protobuf int64 field results in the following unmarshaling error:

body unmarshal json: cannot unmarshal string into Go struct field MyRequest.id of type int64

Simply importing _ "github.com/go-kratos/kratos/v3/encoding/protojson" in main.go (as suggested in issue #3820) does NOT resolve this issue.

What you expected to happen:

How to reproduce it (as minimally and precisely as possible):

Anything else we need to know?:

Root Cause Analysis

  1. protojson codec registers itself under the name "protojson":
    // github.com/go-kratos/kratos/v3/encoding/protojson
    const Name = "protojson"
  2. When a client sends Content-Type: application/json, Kratos HTTP Server resolves the codec name as "json" via CodecForRequest(r, "Content-Type").
  3. As a result, Kratos falls back to the default Go standard library encoding/json codec (registered under "json"), which strictly rejects string-to-int64 unmarshaling for struct fields. The imported "protojson" codec is never invoked.

Solution / Recommended Workaround

Customizing http.RequestDecoder on the http.Server options ensures that any proto.Message request payload is unmarshaled using protojson.UnmarshalOptions (which natively supports string-formatted int64), regardless of whether the HTTP Content-Type is application/json or application/protojson.

Implementation

package encoder

import (
	"encoding/json"
	"io"
	"net/http"

	kerrors "github.com/go-kratos/kratos/v3/errors"
	"google.golang.org/protobuf/encoding/protojson"
	"google.golang.org/protobuf/proto"
)

// CustomRequestDecoder handles both application/json and application/protojson smoothly,
// supporting string-formatted int64 fields (e.g., "id": "123") without forcing clients to change Content-Type.
func CustomRequestDecoder(r *http.Request, v any) error {
	data, err := io.ReadAll(r.Body)
	if err != nil {
		return kerrors.BadRequest("BODY", err.Error())
	}
	if m, ok := v.(proto.Message); ok {
		if err := (protojson.UnmarshalOptions{DiscardUnknown: true}).Unmarshal(data, m); err != nil {
			return kerrors.BadRequest("BODY", err.Error())
		}
		return nil
	}
	if err := json.Unmarshal(data, v); err != nil {
		return kerrors.BadRequest("BODY", err.Error())
	}
	return nil
}

Server Configuration

srv := http.NewServer(
	http.Middleware(...),
	http.RequestDecoder(encoder.CustomRequestDecoder),
)

Suggestion for Kratos Framework

Consider updating the default json codec in github.com/go-kratos/kratos/v3/encoding/json or the default HTTP request decoder to check if the target struct is proto.Message, and fallback to protojson unmarshaling rules for application/json content types.

Environment:

  • Kratos version (use kratos -v): v3.0.0
  • Go version (use go version): 1.25.11
  • OS (e.g: cat /etc/os-release):
  • Others:

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions