Skip to content
protoc-gen-sphere-binding

protoc-gen-sphere-binding

protoc-gen-sphere-binding is a protoc plugin that generates Go struct tags for Sphere binding from .proto files. It is designed to inspect service definitions within your protobuf files and automatically generate corresponding Go struct tags based on sphere binding annotations.

Unlike other protoc plugins that generate new files, this plugin modifies the generated Go structs by injecting appropriate tags for request binding in HTTP handlers.

Installation

To install protoc-gen-sphere-binding, use the following command:

go install github.com/go-sphere/protoc-gen-sphere-binding@latest

Prerequisites

You need to have the sphere binding proto definitions in your project. Add the following dependency to your buf.yaml:

deps:
  - buf.build/go-sphere/binding

Configuration Parameters

The behavior of protoc-gen-sphere-binding can be customized with the following parameters:

  • version: Print the current plugin version and exit. (Default: false)
  • out: The output directory for the modified .pb.go files. (Default: api)
  • auto_remove_json: Automatically remove json tag when sphere binding location is set. (Default: true)
  • binding_aliases: Add additional tag aliases for any binding tag. Format: tag1=alias1,tag2=alias2. Example: query=form,uri=path,db=database. (Default: "")

Usage with Buf

protoc-gen-sphere-binding does not emit new Go files. It rewrites the .pb.go files produced by protoc-gen-go. Official templates therefore run it from a second Buf template, typically buf.binding.yaml, after buf generate:

version: v2
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: github.com/go-sphere/sphere-layout/api
plugins:
  - local: protoc-gen-sphere-binding
    out: api
    opt:
      - paths=source_relative
      - out=api

The corresponding Makefile step is buf generate --template buf.binding.yaml. Putting this plugin in the same buf.gen.yaml as protoc-gen-go is not reliable, because the rewrite must see the already-generated structs.

How It Works

protoc-gen-sphere-binding processes protobuf files and adds Go struct tags based on sphere binding annotations. The plugin works in conjunction with protoc-gen-go and should be run after the standard Go code generation to add the binding tags to the generated structs.

Binding Locations

The plugin supports the following binding locations through the sphere.binding.location annotation:

  • BINDING_LOCATION_JSON: Fields bound to JSON request body (default behavior)
  • BINDING_LOCATION_QUERY: Fields bound to query parameters (adds query tag, removes json tag)
  • BINDING_LOCATION_URI: Fields bound to URI path parameters (adds uri tag, removes json tag)
  • BINDING_LOCATION_HEADER: Fields bound to HTTP headers (adds header tag, removes json tag)
  • BINDING_LOCATION_FORM: Fields bound to form data (adds form tag, removes json tag)

Proto Definition Example

Here’s a comprehensive example showing different binding locations:

syntax = "proto3";

package shared.v1;

import "buf/validate/validate.proto";
import "google/api/annotations.proto";
import "sphere/binding/binding.proto";

service TestService {
  rpc RunTest(RunTestRequest) returns (RunTestResponse) {
    option (google.api.http) = {
      post: "/v1/test/{path_test1}"
      body: "*"
    };
  }
}

message RunTestRequest {
  // URI path parameter
  string path_test1 = 1 [(sphere.binding.location) = BINDING_LOCATION_URI];
  // Fields without binding annotation go to JSON body
  string field_test1 = 2;
  // Query parameter
  string query_test1 = 3 [(sphere.binding.location) = BINDING_LOCATION_QUERY];
  // Header value
  string auth_token = 8 [(sphere.binding.location) = BINDING_LOCATION_HEADER];
  // Custom tags
  repeated int32 ids = 9 [
    (sphere.binding.location) = BINDING_LOCATION_QUERY,
    (sphere.binding.auto_tags) = "custom:\"ids\""
  ];
}

message RunTestResponse {
  string message = 1;
}

// Message with default auto tags
message BodyPathTestRequest {
  option (sphere.binding.default_auto_tags) = "db";
  
  message Request {
    string name = 1;  // Will get: db:"name" json:"name"
    string email = 2; // Will get: db:"email" json:"email"
  }
  Request request = 1;
}

enum TestEnum {
  TEST_ENUM_UNSPECIFIED = 0;
  TEST_ENUM_VALUE1 = 1;
  TEST_ENUM_VALUE2 = 2;
}

Generated Code

After running protoc-gen-go followed by protoc-gen-sphere-binding, the generated Go struct will have appropriate tags:

type RunTestRequest struct {
    state         protoimpl.MessageState `protogen:"open.v1"`
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    PathTest1  string  `protobuf:"bytes,1,opt,name=path_test1,json=pathTest1,proto3" json:"-" uri:"path_test1"`
    FieldTest1 string  `protobuf:"bytes,2,opt,name=field_test1,json=fieldTest1,proto3" json:"field_test1"`
    QueryTest1 string  `protobuf:"bytes,3,opt,name=query_test1,json=queryTest1,proto3" json:"-" query:"query_test1"`
    AuthToken  string  `protobuf:"bytes,8,opt,name=auth_token,json=authToken,proto3" json:"-" header:"auth_token"`
    Ids        []int32 `protobuf:"varint,9,rep,packed,name=ids,proto3" json:"-" query:"ids" custom:"ids"`
}

type BodyPathTestRequest_Request struct {
    state         protoimpl.MessageState `protogen:"open.v1"`
    sizeCache     protoimpl.SizeCache
    unknownFields protoimpl.UnknownFields

    Name  string `protobuf:"bytes,1,opt,name=name,proto3" json:"name" db:"name"`
    Email string `protobuf:"bytes,2,opt,name=email,proto3" json:"email" db:"email"`
}

Advanced Features

Message-Level Configuration

You can set default binding behavior for entire messages:

message SearchRequest {
  option (sphere.binding.default_location) = BINDING_LOCATION_QUERY;
  option (sphere.binding.default_auto_tags) = "validate";
  
  string name = 1;        // Will be bound from query with validate tag
  int32 age = 2;          // Will be bound from query with validate tag
  string email = 3;       // Will be bound from query with validate tag
}

Nested types

Nested message type definitions do not inherit default_location from the enclosing message. A nested JSON object under a QUERY-default parent keeps its json tags. Map-entry types are skipped.

Oneof Support

protoc-gen-go emits each oneof member on a wrapper struct (Message_Field). The binding plugin tags those wrappers, not the parent message field.

Generated HTTP handlers bind the parent request. Gin/httpx binders do not follow oneof wrappers, so QUERY/URI/HEADER oneof members are not populated at runtime. Prefer dedicated request messages for HTTP APIs.

message TestRequest {
  oneof test_oneof {
    option (sphere.binding.default_oneof_location) = BINDING_LOCATION_QUERY;
    option (sphere.binding.default_oneof_auto_tags) = "validate";
    
    string option_a = 1;
    string option_b = 2;
  }
}

JSON codecs also handle oneof awkwardly on the wire.

Custom Tags

Add custom Go struct tags using the auto_tags annotation:

message DatabaseModel {
  option (sphere.binding.default_auto_tags) = "db";
  
  string name = 1;     // Generated: `db:"name" json:"name"`
  string email = 2 [(sphere.binding.auto_tags) = "validate:\"email\""];
  // Generated: `db:"email" json:"email" validate:"email"`
}

Usage in HTTP Handlers

Generated handlers call methods on httpx.Context. You normally do not write this by hand:

func _TestService_RunTest0_HTTP_Handler(srv TestServiceHTTPServer) httpx.Handler {
    return httpz.WithJson(func(ctx httpx.Context) (*RunTestResponse, error) {
        var in RunTestRequest
        if err := ctx.BindJSON(&in); err != nil {
            return nil, err
        }
        if err := ctx.BindQuery(&in); err != nil {
            return nil, err
        }
        if err := ctx.BindURI(&in); err != nil {
            return nil, err
        }
        return srv.RunTest(ctx.Context(), &in)
    })
}

BindHeader and BindForm are used when the corresponding binding locations are present.

Integration with Other Generators

protoc-gen-sphere-binding works best when combined with other Sphere generators:

  1. protoc-gen-go: Generates the base Go structs
  2. protoc-gen-sphere-binding: Adds binding tags to the structs
  3. protoc-gen-sphere: Generates HTTP handlers that use the tagged structs

Best Practices

  1. Use consistent binding locations: Establish patterns for where different types of data should be bound from
  2. Leverage message-level defaults: Use default_location and default_auto_tags to reduce repetition
  3. Be explicit when needed: Override defaults with field-level annotations when necessary
  4. Test your bindings: Verify that the generated tags work with the httpx adapter you actually run
  5. Keep it simple: Avoid overly complex binding patterns that might confuse API consumers

Troubleshooting

Common Issues

  1. Tags not appearing: Make sure you’re running protoc-gen-sphere-binding after protoc-gen-go
  2. Binding not working: Verify that your HTTP framework supports the generated tag format
  3. Conflicts with existing tags: Check for conflicts between generated tags and existing protobuf tags

Debugging

You can verify the generated tags by examining the generated Go files or using reflection:

import (
    "fmt"
    "reflect"
)

func inspectTags() {
    t := reflect.TypeOf(RunTestRequest{})
    for i := 0; i < t.NumField(); i++ {
        field := t.Field(i)
        fmt.Printf("Field: %s, Tags: %s\n", field.Name, field.Tag)
    }
}
Last updated on