Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion examples/error-handler/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/error-handler
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-20250818125809-ff61bcf670dd
github.com/google/uuid v1.6.0
Expand Down
1 change: 1 addition & 0 deletions examples/error-handler/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/go/protovalidate v0.14.0 h1:kr/rC/no+DtRyYX+8KXLDxNnI1rINz0imk5K44ZpZ3A=
buf.build/go/protovalidate v0.14.0/go.mod h1:+F/oISho9MO7gJQNYC2VWLzcO1fTPmaTA08SDYJZncA=
cel.dev/expr v0.23.1 h1:K4KOtPCJQjVggkARsjG9RWXP6O4R73aHeJMa/dmCQQg=
Expand Down
2 changes: 1 addition & 1 deletion examples/market-data-unwrap/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/market-data-unwrap
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v1.1.0
github.com/SebastienMelki/sebuf v0.0.0
google.golang.org/protobuf v1.36.11
Expand Down
1 change: 1 addition & 0 deletions examples/market-data-unwrap/go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1 h1:j9yeqTWEFrtimt8Nng2MIeRrpoCvQzM9/g25XTvqUGg=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1/go.mod h1:tvtbpgaVXZX4g6Pn+AnzFycuRK3MOz5HJfEGeEllXYM=
buf.build/go/protovalidate v1.1.0 h1:pQqEQRpOo4SqS60qkvmhLTTQU9JwzEvdyiqAtXa5SeY=
buf.build/go/protovalidate v1.1.0/go.mod h1:bGZcPiAQDC3ErCHK3t74jSoJDFOs2JH3d7LWuTEIdss=
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
Expand Down
2 changes: 1 addition & 1 deletion examples/multi-service-api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/multi-service-api
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-20250818125809-ff61bcf670dd
google.golang.org/protobuf v1.36.11
Expand Down
2 changes: 1 addition & 1 deletion examples/nested-resources/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/nested-resources
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-20250818125809-ff61bcf670dd
google.golang.org/protobuf v1.36.11
Expand Down
2 changes: 1 addition & 1 deletion examples/restful-crud/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/restful-crud
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-20250818125809-ff61bcf670dd
google.golang.org/protobuf v1.36.11
Expand Down
2 changes: 1 addition & 1 deletion examples/rn-client-demo/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/rn-client-demo
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-00010101000000-000000000000
google.golang.org/protobuf v1.36.11
Expand Down
2 changes: 1 addition & 1 deletion examples/simple-api/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/simple-api
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-20250818125809-ff61bcf670dd
google.golang.org/protobuf v1.36.11
Expand Down
2 changes: 1 addition & 1 deletion examples/ts-client-demo/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/ts-client-demo
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-00010101000000-000000000000
google.golang.org/protobuf v1.36.11
Expand Down
2 changes: 1 addition & 1 deletion examples/validation-showcase/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ module github.com/SebastienMelki/sebuf/examples/validation-showcase
go 1.24.7

require (
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20251209175733-2a1774d88802.1
buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go v1.36.11-20260209202127-80ab13bee0bf.1
buf.build/go/protovalidate v0.14.0
github.com/SebastienMelki/sebuf v0.0.0-20250818125809-ff61bcf670dd
google.golang.org/protobuf v1.36.11
Expand Down
171 changes: 168 additions & 3 deletions internal/clientgen/encoding.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"google.golang.org/protobuf/compiler/protogen"
"google.golang.org/protobuf/reflect/protoreflect"

"github.com/SebastienMelki/sebuf/internal/annotations"
)
Expand Down Expand Up @@ -82,6 +83,57 @@ func collectInt64EncodingMessages(messages []*protogen.Message, contexts *[]*Int
}
}

// Int64WrapperContext holds information about messages that contain nested messages
// with int64 NUMBER encoding — requiring transitive MarshalJSON/UnmarshalJSON.
type Int64WrapperContext struct {
// Message is the wrapper message that needs transitive marshal/unmarshal
Message *protogen.Message
// NestedFields are message-type fields whose type has direct NUMBER encoding
NestedFields []*protogen.Field
}

// collectWrapperContexts finds messages that contain fields whose message type
// has direct int64 NUMBER encoding (i.e., types already in directMsgNames).
func collectWrapperContexts(file *protogen.File, directMsgNames map[string]bool) []*Int64WrapperContext {
var contexts []*Int64WrapperContext
collectWrapperMessages(file.Messages, directMsgNames, &contexts)
return contexts
}

// collectWrapperMessages recursively collects wrapper messages.
func collectWrapperMessages(
messages []*protogen.Message,
directMsgNames map[string]bool,
contexts *[]*Int64WrapperContext,
) {
for _, msg := range messages {
// Skip messages that already have direct NUMBER fields (handled by existing logic)
if directMsgNames[string(msg.Desc.FullName())] {
collectWrapperMessages(msg.Messages, directMsgNames, contexts)
continue
}

var nestedFields []*protogen.Field
for _, field := range msg.Fields {
if field.Desc.Kind() == protoreflect.MessageKind &&
!field.Desc.IsMap() &&
field.Message != nil &&
directMsgNames[string(field.Message.Desc.FullName())] {
nestedFields = append(nestedFields, field)
}
}

if len(nestedFields) > 0 {
*contexts = append(*contexts, &Int64WrapperContext{
Message: msg,
NestedFields: nestedFields,
})
}

collectWrapperMessages(msg.Messages, directMsgNames, contexts)
}
}

// printInt64PrecisionWarning prints a generation-time warning for fields with NUMBER encoding.
func printInt64PrecisionWarning(w io.Writer, field *protogen.Field, messageName string) {
_, _ = w.Write([]byte(
Expand All @@ -94,8 +146,17 @@ func printInt64PrecisionWarning(w io.Writer, field *protogen.Field, messageName
func (g *Generator) generateInt64EncodingFile(file *protogen.File) error {
contexts := collectInt64EncodingContext(file)

// Build set of message full names that have direct NUMBER fields
directMsgNames := make(map[string]bool, len(contexts))
for _, ctx := range contexts {
directMsgNames[string(ctx.Message.Desc.FullName())] = true
}

// Collect wrapper messages whose fields reference messages with NUMBER fields
wrapperContexts := collectWrapperContexts(file, directMsgNames)

// If no messages need int64 encoding, skip generation
if len(contexts) == 0 {
if len(contexts) == 0 && len(wrapperContexts) == 0 {
return nil
}

Expand All @@ -105,9 +166,8 @@ func (g *Generator) generateInt64EncodingFile(file *protogen.File) error {
g.writeEncodingHeader(gf, file)
g.writeInt64EncodingImports(gf)

// Generate marshal/unmarshal for each message
// Generate marshal/unmarshal for messages with direct NUMBER fields
for _, ctx := range contexts {
// Print warnings during generation
for _, field := range ctx.NumberFields {
printInt64PrecisionWarning(os.Stderr, field, ctx.Message.GoIdent.GoName)
}
Expand All @@ -116,6 +176,12 @@ func (g *Generator) generateInt64EncodingFile(file *protogen.File) error {
g.generateInt64UnmarshalJSON(gf, ctx)
}

// Generate transitive marshal/unmarshal for wrapper messages
for _, ctx := range wrapperContexts {
g.generateWrapperMarshalJSON(gf, ctx)
g.generateWrapperUnmarshalJSON(gf, ctx)
}

return nil
}

Expand Down Expand Up @@ -336,3 +402,102 @@ func isUint64Type(field *protogen.Field) bool {
kind := field.Desc.Kind().String()
return kind == kindUint64 || kind == kindFixed64
}

// generateWrapperMarshalJSON generates a MarshalJSON that re-marshals nested
// messages via json.Marshal, so their custom MarshalJSON methods are called.
func (g *Generator) generateWrapperMarshalJSON(gf *protogen.GeneratedFile, ctx *Int64WrapperContext) {
msgName := ctx.Message.GoIdent.GoName

var nestedFieldNames []string
for _, f := range ctx.NestedFields {
nestedFieldNames = append(nestedFieldNames, string(f.Desc.Name()))
}

gf.P("// MarshalJSON implements json.Marshaler for ", msgName, ".")
gf.P(
"// This method re-marshals nested messages that have int64_encoding=NUMBER fields: ",
strings.Join(nestedFieldNames, ", "),
)
gf.P("func (x *", msgName, ") MarshalJSON() ([]byte, error) {")
gf.P("if x == nil {")
gf.P("return []byte(\"null\"), nil")
gf.P("}")
gf.P()
gf.P("// Use protojson for base serialization (handles all other fields correctly)")
gf.P("data, err := protojson.Marshal(x)")
gf.P("if err != nil {")
gf.P("return nil, err")
gf.P("}")
gf.P()
gf.P("// Parse into a map to re-serialize nested messages with custom MarshalJSON")
gf.P("var raw map[string]json.RawMessage")
gf.P("if err := json.Unmarshal(data, &raw); err != nil {")
gf.P("return nil, err")
gf.P("}")
gf.P()

for _, field := range ctx.NestedFields {
jsonName := field.Desc.JSONName()
gf.P("// Re-serialize \"", jsonName, "\" using its custom MarshalJSON")
gf.P("if x.", field.GoName, " != nil {")
gf.P("raw[\"", jsonName, "\"], err = json.Marshal(x.", field.GoName, ")")
gf.P("if err != nil {")
gf.P("return nil, err")
gf.P("}")
gf.P("}")
gf.P()
}

gf.P("return json.Marshal(raw)")
gf.P("}")
gf.P()
}

// generateWrapperUnmarshalJSON generates an UnmarshalJSON that delegates nested
// message parsing to their custom UnmarshalJSON, then converts back for protojson.
func (g *Generator) generateWrapperUnmarshalJSON(gf *protogen.GeneratedFile, ctx *Int64WrapperContext) {
msgName := ctx.Message.GoIdent.GoName

var nestedFieldNames []string
for _, f := range ctx.NestedFields {
nestedFieldNames = append(nestedFieldNames, string(f.Desc.Name()))
}

gf.P("// UnmarshalJSON implements json.Unmarshaler for ", msgName, ".")
gf.P(
"// This method handles nested messages that have int64_encoding=NUMBER fields: ",
strings.Join(nestedFieldNames, ", "),
)
gf.P("func (x *", msgName, ") UnmarshalJSON(data []byte) error {")
gf.P("var raw map[string]json.RawMessage")
gf.P("if err := json.Unmarshal(data, &raw); err != nil {")
gf.P("return err")
gf.P("}")
gf.P()

for _, field := range ctx.NestedFields {
jsonName := field.Desc.JSONName()
gf.P("// Handle \"", jsonName, "\" using its custom UnmarshalJSON")
gf.P("if rawVal, ok := raw[\"", jsonName, "\"]; ok {")
gf.P("inner := &", gf.QualifiedGoIdent(field.Message.GoIdent), "{}")
gf.P("if err := json.Unmarshal(rawVal, inner); err != nil {")
gf.P("return err")
gf.P("}")
gf.P("innerJSON, err := protojson.Marshal(inner)")
gf.P("if err != nil {")
gf.P("return err")
gf.P("}")
gf.P("raw[\"", jsonName, "\"] = innerJSON")
gf.P("}")
gf.P()
}

gf.P("modified, err := json.Marshal(raw)")
gf.P("if err != nil {")
gf.P("return err")
gf.P("}")
gf.P()
gf.P("return protojson.Unmarshal(modified, x)")
gf.P("}")
gf.P()
}
8 changes: 8 additions & 0 deletions internal/clientgen/golden_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ func TestClientGenGoldenFiles(t *testing.T) {
"int64_encoding_encoding.pb.go",
},
},
{
name: "int64 nested encoding (wrapper response)",
protoFile: "int64_nested_encoding.proto",
expectedFiles: []string{
"int64_nested_encoding_client.pb.go",
"int64_nested_encoding_encoding.pb.go",
},
},
{
name: "enum encoding",
protoFile: "enum_encoding.proto",
Expand Down
Loading
Loading