feat(render)!: make msgpack/bson/yaml/toml/protobuf opt-in subpackages

- Move MessagePack, BSON, YAML, TOML and ProtoBuf rendering and binding out
  of core into github.com/gin-gonic/gin/render/<format> subpackages
- Add content-type registries to binding and render that each subpackage
  populates from init() so ShouldBind and Negotiate keep negotiating
- Slim binding.Default to core types and resolve the rest via the registry
- Drop the obsolete nomsgpack build tag and binding_nomsgpack.go
- Cut a minimal JSON-only binary from 13MB to 6.5MB; non-core codecs now
  cost binary size only when their subpackage is imported

BREAKING CHANGE: c.YAML, c.TOML, c.ProtoBuf, c.BSON, c.BindYAML, c.BindTOML,
c.ShouldBindYAML, c.ShouldBindTOML, c.ShouldBindBodyWithYAML and
c.ShouldBindBodyWithTOML are removed, along with binding.MsgPack/BSON/YAML/
TOML/ProtoBuf, the render.MsgPack/BSON/YAML/TOML/ProtoBuf types and the
nomsgpack build tag. Import the matching github.com/gin-gonic/gin/render/<format>
subpackage and use its Render(c, code, obj) / ShouldBind(c, &obj) helpers
(or pass <format>.Binding to c.ShouldBindWith). Importing the subpackage also
re-registers the content type so c.ShouldBind and c.Negotiate work as before.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Bo-Yi Wu 2026-06-21 22:30:25 +08:00
parent d75fcd4c9a
commit d43f6a591b
No known key found for this signature in database
33 changed files with 1005 additions and 1444 deletions

View File

@ -2,8 +2,6 @@
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
package binding
import "net/http"
@ -73,6 +71,11 @@ var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
//
// Bindings for non-core content types (MsgPack, BSON, YAML, TOML, ProtoBuf)
// are no longer registered here. Import the matching subpackage under
// github.com/gin-gonic/gin/render/<format> to opt in; its init() registers the
// binding with Default via Register so content-type negotiation keeps working.
var (
JSON BindingBody = jsonBinding{}
XML BindingBody = xmlBinding{}
@ -80,18 +83,27 @@ var (
Query Binding = queryBinding{}
FormPost Binding = formPostBinding{}
FormMultipart Binding = formMultipartBinding{}
ProtoBuf BindingBody = protobufBinding{}
MsgPack BindingBody = msgpackBinding{}
YAML BindingBody = yamlBinding{}
Uri BindingUri = uriBinding{}
Header Binding = headerBinding{}
Plain BindingBody = plainBinding{}
TOML BindingBody = tomlBinding{}
BSON BindingBody = bsonBinding{}
)
// registry maps a content type to the Binding registered for it by an optional
// format subpackage. It is only written from init() functions before main runs,
// so it needs no synchronization.
var registry = map[string]Binding{}
// Register associates a Binding with one content type so that Default (and
// therefore ShouldBind/Bind content-type negotiation) can resolve it. It is
// intended to be called from an init() function in a format subpackage, e.g.
// github.com/gin-gonic/gin/render/msgpack.
func Register(contentType string, b Binding) {
registry[contentType] = b
}
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
// and the content type. Content types served by an optional format subpackage
// are resolved through the registry populated by Register.
func Default(method, contentType string) Binding {
if method == http.MethodGet {
return Form
@ -102,21 +114,22 @@ func Default(method, contentType string) Binding {
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEMSGPACK, MIMEMSGPACK2:
return MsgPack
case MIMEYAML, MIMEYAML2:
return YAML
case MIMETOML:
return TOML
case MIMEMultipartPOSTForm:
return FormMultipart
case MIMEBSON:
return BSON
default: // case MIMEPOSTForm:
case MIMEPOSTForm:
return Form
}
if b, ok := registry[contentType]; ok {
return b
}
return Form
}
// Validate runs the registered StructValidator against obj. Format subpackages
// call it after decoding so that the `binding:""` struct tags keep working.
func Validate(obj any) error {
return validate(obj)
}
func validate(obj any) error {

View File

@ -1,59 +0,0 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
package binding
import (
"bytes"
"net/http"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestBindingMsgPack(t *testing.T) {
test := FooStruct{
Foo: "bar",
}
h := new(codec.MsgpackHandle)
assert.NotNil(t, h)
buf := bytes.NewBuffer([]byte{})
assert.NotNil(t, buf)
err := codec.NewEncoder(buf, h).Encode(test)
require.NoError(t, err)
data := buf.Bytes()
testMsgPackBodyBinding(t,
MsgPack, "msgpack",
"/", "/",
string(data), string(data[1:]))
}
func testMsgPackBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := FooStruct{}
req := requestWithBody(http.MethodPost, path, body)
req.Header.Add("Content-Type", MIMEMSGPACK)
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "bar", obj.Foo)
obj = FooStruct{}
req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEMSGPACK)
err = MsgPack.Bind(req, &obj)
require.Error(t, err)
}
func TestBindingDefaultMsgPack(t *testing.T) {
assert.Equal(t, MsgPack, Default(http.MethodPost, MIMEMSGPACK))
assert.Equal(t, MsgPack, Default(http.MethodPut, MIMEMSGPACK2))
}

View File

@ -1,121 +0,0 @@
// Copyright 2020 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build nomsgpack
package binding
import "net/http"
// Content-Type MIME of the most common data formats.
const (
MIMEJSON = "application/json"
MIMEHTML = "text/html"
MIMEXML = "application/xml"
MIMEXML2 = "text/xml"
MIMEPlain = "text/plain"
MIMEPOSTForm = "application/x-www-form-urlencoded"
MIMEMultipartPOSTForm = "multipart/form-data"
MIMEPROTOBUF = "application/x-protobuf"
MIMEYAML = "application/x-yaml"
MIMEYAML2 = "application/yaml"
MIMETOML = "application/toml"
MIMEBSON = "application/bson"
)
// Binding describes the interface which needs to be implemented for binding the
// data present in the request such as JSON request body, query parameters or
// the form POST.
type Binding interface {
Name() string
Bind(*http.Request, any) error
}
// BindingBody adds BindBody method to Binding. BindBody is similar with Bind,
// but it reads the body from supplied bytes instead of req.Body.
type BindingBody interface {
Binding
BindBody([]byte, any) error
}
// BindingUri adds BindUri method to Binding. BindUri is similar with Bind,
// but it reads the Params.
type BindingUri interface {
Name() string
BindUri(map[string][]string, any) error
}
// StructValidator is the minimal interface which needs to be implemented in
// order for it to be used as the validator engine for ensuring the correctness
// of the request. Gin provides a default implementation for this using
// https://github.com/go-playground/validator/tree/v10.6.1.
type StructValidator interface {
// ValidateStruct can receive any kind of type and it should never panic, even if the configuration is not right.
// If the received type is not a struct, any validation should be skipped and nil must be returned.
// If the received type is a struct or pointer to a struct, the validation should be performed.
// If the struct is not valid or the validation itself fails, a descriptive error should be returned.
// Otherwise nil must be returned.
ValidateStruct(any) error
// Engine returns the underlying validator engine which powers the
// StructValidator implementation.
Engine() any
}
// Validator is the default validator which implements the StructValidator
// interface. It uses https://github.com/go-playground/validator/tree/v10.6.1
// under the hood.
var Validator StructValidator = &defaultValidator{}
// These implement the Binding interface and can be used to bind the data
// present in the request to struct instances.
var (
JSON = jsonBinding{}
XML = xmlBinding{}
Form = formBinding{}
Query = queryBinding{}
FormPost = formPostBinding{}
FormMultipart = formMultipartBinding{}
ProtoBuf = protobufBinding{}
YAML = yamlBinding{}
Uri = uriBinding{}
Header = headerBinding{}
TOML = tomlBinding{}
Plain = plainBinding{}
BSON BindingBody = bsonBinding{}
)
// Default returns the appropriate Binding instance based on the HTTP method
// and the content type.
func Default(method, contentType string) Binding {
if method == "GET" {
return Form
}
switch contentType {
case MIMEJSON:
return JSON
case MIMEXML, MIMEXML2:
return XML
case MIMEPROTOBUF:
return ProtoBuf
case MIMEYAML, MIMEYAML2:
return YAML
case MIMEMultipartPOSTForm:
return FormMultipart
case MIMETOML:
return TOML
case MIMEBSON:
return BSON
default: // case MIMEPOSTForm:
return Form
}
}
func validate(obj any) error {
if Validator == nil {
return nil
}
return Validator.ValidateStruct(obj)
}

View File

@ -18,11 +18,8 @@ import (
"testing"
"time"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
type appkey struct {
@ -162,20 +159,6 @@ func TestBindingDefault(t *testing.T) {
assert.Equal(t, FormMultipart, Default(http.MethodPost, MIMEMultipartPOSTForm))
assert.Equal(t, FormMultipart, Default(http.MethodPut, MIMEMultipartPOSTForm))
assert.Equal(t, ProtoBuf, Default(http.MethodPost, MIMEPROTOBUF))
assert.Equal(t, ProtoBuf, Default(http.MethodPut, MIMEPROTOBUF))
assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML))
assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML))
assert.Equal(t, YAML, Default(http.MethodPost, MIMEYAML2))
assert.Equal(t, YAML, Default(http.MethodPut, MIMEYAML2))
assert.Equal(t, TOML, Default(http.MethodPost, MIMETOML))
assert.Equal(t, TOML, Default(http.MethodPut, MIMETOML))
assert.Equal(t, BSON, Default(http.MethodPost, MIMEBSON))
assert.Equal(t, BSON, Default(http.MethodPut, MIMEBSON))
}
func TestBindingJSONNilBody(t *testing.T) {
@ -465,41 +448,6 @@ func TestBindingXMLFail(t *testing.T) {
"<map><foo>bar<foo></map>", "<map><bar>foo</bar></map>")
}
func TestBindingTOML(t *testing.T) {
testBodyBinding(t,
TOML, "toml",
"/", "/",
`foo="bar"`, `bar="foo"`)
}
func TestBindingTOMLFail(t *testing.T) {
testBodyBindingFail(t,
TOML, "toml",
"/", "/",
`foo=\n"bar"`, `bar="foo"`)
}
func TestBindingYAML(t *testing.T) {
testBodyBinding(t,
YAML, "yaml",
"/", "/",
`foo: bar`, `bar: foo`)
}
func TestBindingYAMLStringMap(t *testing.T) {
// YAML is a superset of JSON, so the test below is JSON (to avoid newlines)
testBodyBindingStringMap(t, YAML,
"/", "/",
`{"foo": "bar", "hello": "world"}`, `{"nested": {"foo": "bar"}}`)
}
func TestBindingYAMLFail(t *testing.T) {
testBodyBindingFail(t,
YAML, "yaml",
"/", "/",
`foo:\nbar`, `bar: foo`)
}
func createFormPostRequest(t *testing.T) *http.Request {
req, err := http.NewRequest(http.MethodPost, "/?foo=getfoo&bar=getbar", bytes.NewBufferString("foo=bar&bar=foo"))
require.NoError(t, err)
@ -711,42 +659,6 @@ func TestBindingFormMultipartForMapFail(t *testing.T) {
require.Error(t, err)
}
func TestBindingProtoBuf(t *testing.T) {
test := &protoexample.Test{
Label: proto.String("yes"),
}
data, _ := proto.Marshal(test)
testProtoBodyBinding(t,
ProtoBuf, "protobuf",
"/", "/",
string(data), string(data[1:]))
}
func TestBindingProtoBufFail(t *testing.T) {
test := &protoexample.Test{
Label: proto.String("yes"),
}
data, _ := proto.Marshal(test)
testProtoBodyBindingFail(t,
ProtoBuf, "protobuf",
"/", "/",
string(data), string(data[1:]))
}
func TestBindingBSON(t *testing.T) {
var obj FooStruct
obj.Foo = "bar"
data, _ := bson.Marshal(&obj)
testBodyBinding(t,
BSON, "bson",
"/", "/",
string(data),
// note: for badbody, we remove first byte to make it invalid
string(data[1:]))
}
func TestValidationFails(t *testing.T) {
var obj FooStruct
req := requestWithBody(http.MethodPost, "/", `{"bar": "foo"}`)
@ -1340,23 +1252,6 @@ func testBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, bad
require.Error(t, err)
}
func testProtoBodyBinding(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := protoexample.Test{}
req := requestWithBody(http.MethodPost, path, body)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err := b.Bind(req, &obj)
require.NoError(t, err)
assert.Equal(t, "yes", *obj.Label)
obj = protoexample.Test{}
req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = ProtoBuf.Bind(req, &obj)
require.Error(t, err)
}
type hook struct{}
func (h hook) Read([]byte) (int, error) {
@ -1403,31 +1298,6 @@ func TestPlainBinding(t *testing.T) {
require.NoError(t, p.Bind(req, ptr))
}
func testProtoBodyBindingFail(t *testing.T, b Binding, name, path, badPath, body, badBody string) {
assert.Equal(t, name, b.Name())
obj := protoexample.Test{}
req := requestWithBody(http.MethodPost, path, body)
req.Body = io.NopCloser(&hook{})
req.Header.Add("Content-Type", MIMEPROTOBUF)
err := b.Bind(req, &obj)
require.Error(t, err)
invalidobj := FooStruct{}
req.Body = io.NopCloser(strings.NewReader(`{"msg":"hello"}`))
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = b.Bind(req, &invalidobj)
require.Error(t, err)
assert.Equal(t, "obj is not ProtoMessage", err.Error())
obj = protoexample.Test{}
req = requestWithBody(http.MethodPost, badPath, badBody)
req.Header.Add("Content-Type", MIMEPROTOBUF)
err = ProtoBuf.Bind(req, &obj)
require.Error(t, err)
}
func requestWithBody(method, path, body string) (req *http.Request) {
req, _ = http.NewRequest(method, path, bytes.NewBufferString(body))
return

View File

@ -1,30 +0,0 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"io"
"net/http"
"go.mongodb.org/mongo-driver/v2/bson"
)
type bsonBinding struct{}
func (bsonBinding) Name() string {
return "bson"
}
func (b bsonBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
if err == nil {
err = b.BindBody(buf, obj)
}
return err
}
func (bsonBinding) BindBody(body []byte, obj any) error {
return bson.Unmarshal(body, obj)
}

View File

@ -1,37 +0,0 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
package binding
import (
"bytes"
"io"
"net/http"
"github.com/ugorji/go/codec"
)
type msgpackBinding struct{}
func (msgpackBinding) Name() string {
return "msgpack"
}
func (msgpackBinding) Bind(req *http.Request, obj any) error {
return decodeMsgPack(req.Body, obj)
}
func (msgpackBinding) BindBody(body []byte, obj any) error {
return decodeMsgPack(bytes.NewReader(body), obj)
}
func decodeMsgPack(r io.Reader, obj any) error {
cdc := new(codec.MsgpackHandle)
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -1,34 +0,0 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
package binding
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestMsgpackBindingBindBody(t *testing.T) {
type teststruct struct {
Foo string `msgpack:"foo"`
}
var s teststruct
err := msgpackBinding{}.BindBody(msgpackBody(t, teststruct{"FOO"}), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}
func msgpackBody(t *testing.T, obj any) []byte {
var bs bytes.Buffer
h := &codec.MsgpackHandle{}
err := codec.NewEncoder(&bs, h).Encode(obj)
require.NoError(t, err)
return bs.Bytes()
}

View File

@ -1,41 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"errors"
"io"
"net/http"
"google.golang.org/protobuf/proto"
)
type protobufBinding struct{}
func (protobufBinding) Name() string {
return "protobuf"
}
func (b protobufBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
if err != nil {
return err
}
return b.BindBody(buf, obj)
}
func (protobufBinding) BindBody(body []byte, obj any) error {
msg, ok := obj.(proto.Message)
if !ok {
return errors.New("obj is not ProtoMessage")
}
if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return validate(obj), but until now we can't add
// `binding:""` to the struct which automatically generate by gen-proto
return nil
// return validate(obj)
}

View File

@ -1,35 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"io"
"net/http"
"github.com/pelletier/go-toml/v2"
)
type tomlBinding struct{}
func (tomlBinding) Name() string {
return "toml"
}
func (tomlBinding) Bind(req *http.Request, obj any) error {
return decodeToml(req.Body, obj)
}
func (tomlBinding) BindBody(body []byte, obj any) error {
return decodeToml(bytes.NewReader(body), obj)
}
func decodeToml(r io.Reader, obj any) error {
decoder := toml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -1,22 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestTOMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `toml:"foo"`
}
tomlBody := `foo="FOO"`
err := tomlBinding{}.BindBody([]byte(tomlBody), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -1,35 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"bytes"
"io"
"net/http"
"github.com/goccy/go-yaml"
)
type yamlBinding struct{}
func (yamlBinding) Name() string {
return "yaml"
}
func (yamlBinding) Bind(req *http.Request, obj any) error {
return decodeYAML(req.Body, obj)
}
func (yamlBinding) BindBody(body []byte, obj any) error {
return decodeYAML(bytes.NewReader(body), obj)
}
func decodeYAML(r io.Reader, obj any) error {
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return validate(obj)
}

View File

@ -1,21 +0,0 @@
// Copyright 2019 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package binding
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestYAMLBindingBindBody(t *testing.T) {
var s struct {
Foo string `yaml:"foo"`
}
err := yamlBinding{}.BindBody([]byte("foo: FOO"), &s)
require.NoError(t, err)
assert.Equal(t, "FOO", s.Foo)
}

View File

@ -774,16 +774,6 @@ func (c *Context) BindQuery(obj any) error {
return c.MustBindWith(obj, binding.Query)
}
// BindYAML is a shortcut for c.MustBindWith(obj, binding.YAML).
func (c *Context) BindYAML(obj any) error {
return c.MustBindWith(obj, binding.YAML)
}
// BindTOML is a shortcut for c.MustBindWith(obj, binding.TOML).
func (c *Context) BindTOML(obj any) error {
return c.MustBindWith(obj, binding.TOML)
}
// BindPlain is a shortcut for c.MustBindWith(obj, binding.Plain).
func (c *Context) BindPlain(obj any) error {
return c.MustBindWith(obj, binding.Plain)
@ -880,18 +870,6 @@ func (c *Context) ShouldBindQuery(obj any) error {
return c.ShouldBindWith(obj, binding.Query)
}
// ShouldBindYAML is a shortcut for c.ShouldBindWith(obj, binding.YAML).
// It works like ShouldBindJSON but binds the request body as YAML data.
func (c *Context) ShouldBindYAML(obj any) error {
return c.ShouldBindWith(obj, binding.YAML)
}
// ShouldBindTOML is a shortcut for c.ShouldBindWith(obj, binding.TOML).
// It works like ShouldBindJSON but binds the request body as TOML data.
func (c *Context) ShouldBindTOML(obj any) error {
return c.ShouldBindWith(obj, binding.TOML)
}
// ShouldBindPlain is a shortcut for c.ShouldBindWith(obj, binding.Plain).
// It works like ShouldBindJSON but binds plain text data from the request body.
func (c *Context) ShouldBindPlain(obj any) error {
@ -952,16 +930,6 @@ func (c *Context) ShouldBindBodyWithXML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.XML)
}
// ShouldBindBodyWithYAML is a shortcut for c.ShouldBindBodyWith(obj, binding.YAML).
func (c *Context) ShouldBindBodyWithYAML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.YAML)
}
// ShouldBindBodyWithTOML is a shortcut for c.ShouldBindBodyWith(obj, binding.TOML).
func (c *Context) ShouldBindBodyWithTOML(obj any) error {
return c.ShouldBindBodyWith(obj, binding.TOML)
}
// ShouldBindBodyWithPlain is a shortcut for c.ShouldBindBodyWith(obj, binding.Plain).
func (c *Context) ShouldBindBodyWithPlain(obj any) error {
return c.ShouldBindBodyWith(obj, binding.Plain)
@ -1257,26 +1225,6 @@ func (c *Context) PDF(code int, data []byte) {
c.Render(code, render.PDF{Data: data})
}
// YAML serializes the given struct as YAML into the response body.
func (c *Context) YAML(code int, obj any) {
c.Render(code, render.YAML{Data: obj})
}
// TOML serializes the given struct as TOML into the response body.
func (c *Context) TOML(code int, obj any) {
c.Render(code, render.TOML{Data: obj})
}
// ProtoBuf serializes the given struct as ProtoBuf into the response body.
func (c *Context) ProtoBuf(code int, obj any) {
c.Render(code, render.ProtoBuf{Data: obj})
}
// BSON serializes the given struct as BSON into the response body.
func (c *Context) BSON(code int, obj any) {
c.Render(code, render.BSON{Data: obj})
}
// String writes the given string into the response body.
func (c *Context) String(code int, format string, values ...any) {
c.Render(code, render.String{Format: format, Data: values})
@ -1389,7 +1337,8 @@ type Negotiate struct {
// Negotiate calls different Render according to acceptable Accept format.
func (c *Context) Negotiate(code int, config Negotiate) {
switch c.NegotiateFormat(config.Offered...) {
format := c.NegotiateFormat(config.Offered...)
switch format {
case binding.MIMEJSON:
data := chooseData(config.JSONData, config.Data)
c.JSON(code, data)
@ -1402,23 +1351,26 @@ func (c *Context) Negotiate(code int, config Negotiate) {
data := chooseData(config.XMLData, config.Data)
c.XML(code, data)
case binding.MIMEYAML, binding.MIMEYAML2:
data := chooseData(config.YAMLData, config.Data)
c.YAML(code, data)
case binding.MIMETOML:
data := chooseData(config.TOMLData, config.Data)
c.TOML(code, data)
case binding.MIMEPROTOBUF:
data := chooseData(config.PROTOBUFData, config.Data)
c.ProtoBuf(code, data)
case binding.MIMEBSON:
data := chooseData(config.BSONData, config.Data)
c.BSON(code, data)
default:
// Non-core formats (YAML, TOML, ProtoBuf, BSON, ...) are served through
// the render registry, which is populated only when the matching
// github.com/gin-gonic/gin/render/<format> subpackage is imported.
data := config.Data
switch format {
case binding.MIMEYAML, binding.MIMEYAML2:
data = chooseData(config.YAMLData, config.Data)
case binding.MIMETOML:
data = chooseData(config.TOMLData, config.Data)
case binding.MIMEPROTOBUF:
data = chooseData(config.PROTOBUFData, config.Data)
case binding.MIMEBSON:
data = chooseData(config.BSONData, config.Data)
}
if r, ok := render.Negotiate(format, data); ok {
c.Render(code, r)
return
}
c.AbortWithError(http.StatusNotAcceptable, errors.New("the accepted formats are not offered by the server")) //nolint: errcheck
}
}

View File

@ -30,11 +30,8 @@ import (
"github.com/gin-contrib/sse"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/codec/json"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
var _ context.Context = (*Context)(nil)
@ -1511,56 +1508,6 @@ func TestContextRenderUTF8Attachment(t *testing.T) {
assert.Equal(t, `attachment; filename*=UTF-8''`+url.QueryEscape(newFilename), w.Header().Get("Content-Disposition"))
}
// TestContextRenderYAML tests that the response is serialized as YAML
// and Content-Type is set to application/yaml
func TestContextRenderYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.YAML(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextRenderTOML tests that the response is serialized as TOML
// and Content-Type is set to application/toml
func TestContextRenderTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.TOML(http.StatusCreated, H{"foo": "bar"})
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, "foo = 'bar'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
}
// TestContextRenderProtoBuf tests that the response is serialized as ProtoBuf
// and Content-Type is set to application/x-protobuf
// and we just use the example protobuf to check if the response is correct
func TestContextRenderProtoBuf(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
reps := []int64{int64(1), int64(2)}
label := "test"
data := &testdata.Test{
Label: &label,
Reps: reps,
}
c.ProtoBuf(http.StatusCreated, data)
protoData, err := proto.Marshal(data)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}
func TestContextHeaders(t *testing.T) {
c, _ := CreateTestContext(httptest.NewRecorder())
c.Header("Content-Type", "text/plain")
@ -1657,36 +1604,6 @@ func TestContextNegotiationWithXML(t *testing.T) {
assert.Equal(t, "application/xml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEYAML, MIMEXML, MIMEJSON, MIMETOML, MIMEYAML2},
Data: H{"foo": "bar"},
})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "foo: bar\n", w.Body.String())
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMETOML, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2},
Data: H{"foo": "bar"},
})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "foo = 'bar'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithHTML(t *testing.T) {
w := httptest.NewRecorder()
c, router := CreateTestContext(w)
@ -1705,49 +1622,6 @@ func TestContextNegotiationWithHTML(t *testing.T) {
assert.Equal(t, "text/html; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithPROTOBUF(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request = httptest.NewRequest(http.MethodPost, "/", nil)
reps := []int64{int64(1), int64(2)}
label := "test"
data := &testdata.Test{
Label: &label,
Reps: reps,
}
c.Negotiate(http.StatusCreated, Negotiate{
Offered: []string{MIMEPROTOBUF, MIMEJSON, MIMEXML},
Data: data,
})
// Marshal original data for comparison
protoData, err := proto.Marshal(data)
require.NoError(t, err)
assert.Equal(t, http.StatusCreated, w.Code)
assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}
func TestContextNegotiationWithBSON(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "", nil)
c.Negotiate(http.StatusOK, Negotiate{
Offered: []string{MIMEBSON, MIMEXML, MIMEJSON, MIMEYAML, MIMEYAML2},
Data: H{"foo": "bar"},
})
bData, _ := bson.Marshal(H{"foo": "bar"})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, string(bData), w.Body.String())
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
}
func TestContextNegotiationNotSupport(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -2279,40 +2153,6 @@ func TestContextBindWithQuery(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
require.NoError(t, c.BindYAML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBindWithTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo = 'bar'\nbar = 'foo'"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `toml:"foo"`
Bar string `toml:"bar"`
}
require.NoError(t, c.BindTOML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBadAutoBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -2453,40 +2293,6 @@ func TestContextShouldBindWithQuery(t *testing.T) {
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithYAML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo: bar\nbar: foo"))
c.Request.Header.Add("Content-Type", MIMEXML) // set fake content-type
var obj struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar"`
}
require.NoError(t, c.ShouldBindYAML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextShouldBindWithTOML(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/", strings.NewReader("foo='bar'\nbar= 'foo'"))
c.Request.Header.Add("Content-Type", MIMETOML) // set fake content-type
var obj struct {
Foo string `toml:"foo"`
Bar string `toml:"bar"`
}
require.NoError(t, c.ShouldBindTOML(&obj))
assert.Equal(t, "foo", obj.Bar)
assert.Equal(t, "bar", obj.Foo)
assert.Equal(t, 0, w.Body.Len())
}
func TestContextBadAutoShouldBind(t *testing.T) {
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
@ -2604,16 +2410,6 @@ func TestContextShouldBindBodyWithJSON(t *testing.T) {
<foo>FOO</foo>
</root>`,
},
{
name: " JSON & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " JSON & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo=FOO`,
},
} {
t.Logf("testing: %s", tt.name)
@ -2636,16 +2432,6 @@ func TestContextShouldBindBodyWithJSON(t *testing.T) {
require.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
if tt.bindingBody == binding.YAML {
require.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
if tt.bindingBody == binding.TOML {
require.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
}
}
@ -2668,16 +2454,6 @@ func TestContextShouldBindBodyWithXML(t *testing.T) {
<foo>FOO</foo>
</root>`,
},
{
name: " XML & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " XML & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo=FOO`,
},
} {
t.Logf("testing: %s", tt.name)
@ -2700,145 +2476,6 @@ func TestContextShouldBindBodyWithXML(t *testing.T) {
require.NoError(t, c.ShouldBindBodyWithXML(&objXML))
assert.Equal(t, typeXML{"FOO"}, objXML)
}
if tt.bindingBody == binding.YAML {
require.Error(t, c.ShouldBindBodyWithXML(&objXML))
assert.Equal(t, typeXML{}, objXML)
}
if tt.bindingBody == binding.TOML {
require.Error(t, c.ShouldBindBodyWithXML(&objXML))
assert.Equal(t, typeXML{}, objXML)
}
}
}
func TestContextShouldBindBodyWithYAML(t *testing.T) {
for _, tt := range []struct {
name string
bindingBody binding.BindingBody
body string
}{
{
name: " YAML & JSON-BODY ",
bindingBody: binding.JSON,
body: `{"foo":"FOO"}`,
},
{
name: " YAML & XML-BODY ",
bindingBody: binding.XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: " YAML & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " YAML & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo=FOO`,
},
} {
t.Logf("testing: %s", tt.name)
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tt.body))
type typeYAML struct {
Foo string `yaml:"foo" binding:"required"`
}
objYAML := typeYAML{}
// YAML belongs to a super collection of JSON, so JSON can be parsed by YAML
if tt.bindingBody == binding.JSON {
require.NoError(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{"FOO"}, objYAML)
}
if tt.bindingBody == binding.XML {
require.Error(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{}, objYAML)
}
if tt.bindingBody == binding.YAML {
require.NoError(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{"FOO"}, objYAML)
}
if tt.bindingBody == binding.TOML {
require.Error(t, c.ShouldBindBodyWithYAML(&objYAML))
assert.Equal(t, typeYAML{}, objYAML)
}
}
}
func TestContextShouldBindBodyWithTOML(t *testing.T) {
for _, tt := range []struct {
name string
bindingBody binding.BindingBody
body string
}{
{
name: " TOML & JSON-BODY ",
bindingBody: binding.JSON,
body: `{"foo":"FOO"}`,
},
{
name: " TOML & XML-BODY ",
bindingBody: binding.XML,
body: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<foo>FOO</foo>
</root>`,
},
{
name: " TOML & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " TOML & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo = 'FOO'`,
},
} {
t.Logf("testing: %s", tt.name)
w := httptest.NewRecorder()
c, _ := CreateTestContext(w)
c.Request, _ = http.NewRequest(http.MethodPost, "/", bytes.NewBufferString(tt.body))
type typeTOML struct {
Foo string `toml:"foo" binding:"required"`
}
objTOML := typeTOML{}
if tt.bindingBody == binding.JSON {
require.Error(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{}, objTOML)
}
if tt.bindingBody == binding.XML {
require.Error(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{}, objTOML)
}
if tt.bindingBody == binding.YAML {
require.Error(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{}, objTOML)
}
if tt.bindingBody == binding.TOML {
require.NoError(t, c.ShouldBindBodyWithTOML(&objTOML))
assert.Equal(t, typeTOML{"FOO"}, objTOML)
}
}
}
@ -2861,16 +2498,6 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) {
<foo>FOO</foo>
</root>`,
},
{
name: " JSON & YAML-BODY ",
bindingBody: binding.YAML,
body: `foo: FOO`,
},
{
name: " JSON & TOM-BODY ",
bindingBody: binding.TOML,
body: `foo=FOO`,
},
{
name: " JSON & Plain-BODY ",
bindingBody: binding.Plain,
@ -2904,16 +2531,6 @@ func TestContextShouldBindBodyWithPlain(t *testing.T) {
require.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
if tt.bindingBody == binding.YAML {
require.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
if tt.bindingBody == binding.TOML {
require.Error(t, c.ShouldBindBodyWithJSON(&objJSON))
assert.Equal(t, typeJSON{}, objJSON)
}
}
}

View File

@ -1,34 +0,0 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http"
"go.mongodb.org/mongo-driver/v2/bson"
)
// BSON contains the given interface object.
type BSON struct {
Data any
}
var bsonContentType = []string{"application/bson"}
// Render (BSON) marshals the given interface object and writes data with custom ContentType.
func (r BSON) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := bson.Marshal(&r.Data)
if err == nil {
_, err = w.Write(bytes)
}
return err
}
// WriteContentType (BSONBuf) writes BSONBuf ContentType.
func (r BSON) WriteContentType(w http.ResponseWriter) {
writeContentType(w, bsonContentType)
}

109
render/bson/bson.go Normal file
View File

@ -0,0 +1,109 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// Package bson provides optional BSON rendering and binding for gin.
//
// BSON support is no longer compiled into the core gin module. Import this
// package to opt back in:
//
// import (
// "github.com/gin-gonic/gin"
// "github.com/gin-gonic/gin/render/bson"
// )
//
// bson.Render(c, http.StatusOK, obj) // write a BSON response
// bson.ShouldBind(c, &obj) // decode a BSON request body
//
// Importing the package registers the binding and renderer for the
// "application/bson" content type so that the content-type negotiation done by
// c.ShouldBind and c.Negotiate keeps working.
package bson
import (
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"go.mongodb.org/mongo-driver/v2/bson"
)
// MIMEBSON is the content type handled by this package.
const MIMEBSON = binding.MIMEBSON
var contentType = []string{"application/bson"}
func init() {
binding.Register(MIMEBSON, Binding)
render.Register(MIMEBSON, func(data any) render.Render { return renderer{Data: data} })
}
// renderer implements render.Render for BSON responses.
type renderer struct {
Data any
}
var _ render.Render = renderer{}
// Render marshals the data as BSON and writes it with the BSON content type.
func (r renderer) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := bson.Marshal(&r.Data)
if err == nil {
_, err = w.Write(bytes)
}
return err
}
// WriteContentType writes the BSON content type.
func (r renderer) WriteContentType(w http.ResponseWriter) {
writeContentType(w, contentType)
}
// Render writes obj to the response as BSON with status code. It is the
// drop-in replacement for the former c.BSON(code, obj).
func Render(c *gin.Context, code int, obj any) {
c.Render(code, renderer{Data: obj})
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
// Binding decodes BSON request bodies. It can be passed to
// c.ShouldBindWith / c.MustBindWith.
var Binding binding.BindingBody = bsonBinding{}
type bsonBinding struct{}
func (bsonBinding) Name() string {
return "bson"
}
func (b bsonBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
if err == nil {
err = b.BindBody(buf, obj)
}
return err
}
func (bsonBinding) BindBody(body []byte, obj any) error {
return bson.Unmarshal(body, obj)
}
// Bind binds the BSON request body to obj, aborting with HTTP 400 on error.
func Bind(c *gin.Context, obj any) error {
return c.MustBindWith(obj, Binding)
}
// ShouldBind binds the BSON request body to obj without aborting.
func ShouldBind(c *gin.Context, obj any) error {
return c.ShouldBindWith(obj, Binding)
}

58
render/bson/bson_test.go Normal file
View File

@ -0,0 +1,58 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package bson
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
)
type payload struct {
Foo string `bson:"foo"`
Bar string `bson:"bar"`
}
func TestRender(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
var got payload
require.NoError(t, bson.Unmarshal(w.Body.Bytes(), &got))
assert.Equal(t, "foo", got.Foo)
assert.Equal(t, "bar", got.Bar)
}
func TestBinding(t *testing.T) {
body, err := bson.Marshal(payload{Foo: "foo", Bar: "bar"})
require.NoError(t, err)
var out payload
require.NoError(t, Binding.BindBody(body, &out))
assert.Equal(t, "foo", out.Foo)
assert.Equal(t, "bar", out.Bar)
assert.Equal(t, "bson", Binding.Name())
}
func TestRegistration(t *testing.T) {
assert.Equal(t, Binding, binding.Default(http.MethodPost, MIMEBSON))
r, ok := render.Negotiate(MIMEBSON, payload{Foo: "x"})
assert.True(t, ok)
assert.NotNil(t, r)
}

View File

@ -1,43 +0,0 @@
// Copyright 2017 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
package render
import (
"net/http"
"github.com/ugorji/go/codec"
)
// Check interface implemented here to support go build tag nomsgpack.
// See: https://github.com/gin-gonic/gin/pull/1852/
var (
_ Render = MsgPack{}
)
// MsgPack contains the given interface object.
type MsgPack struct {
Data any
}
var msgpackContentType = []string{"application/msgpack; charset=utf-8"}
// WriteContentType (MsgPack) writes MsgPack ContentType.
func (r MsgPack) WriteContentType(w http.ResponseWriter) {
writeContentType(w, msgpackContentType)
}
// Render (MsgPack) encodes the given interface object and writes data with custom ContentType.
func (r MsgPack) Render(w http.ResponseWriter) error {
return WriteMsgPack(w, r.Data)
}
// WriteMsgPack writes MsgPack ContentType and encodes the given interface object.
func WriteMsgPack(w http.ResponseWriter, obj any) error {
writeContentType(w, msgpackContentType)
var mh codec.MsgpackHandle
return codec.NewEncoder(w, &mh).Encode(obj)
}

125
render/msgpack/msgpack.go Normal file
View File

@ -0,0 +1,125 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// Package msgpack provides optional MessagePack rendering and binding for gin.
//
// MessagePack support is no longer compiled into the core gin module. Import
// this package (for its helpers, or with a blank identifier purely for the
// init-time registration) to opt back in:
//
// import (
// "github.com/gin-gonic/gin"
// "github.com/gin-gonic/gin/render/msgpack"
// )
//
// msgpack.Render(c, http.StatusOK, obj) // write a MessagePack response
// msgpack.ShouldBind(c, &obj) // decode a MessagePack request body
//
// Importing the package also registers the binding and renderer for the
// "application/x-msgpack" and "application/msgpack" content types, so the
// content-type negotiation done by c.ShouldBind and c.Negotiate keeps working.
package msgpack
import (
"bytes"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/ugorji/go/codec"
)
// Content types handled by this package.
const (
MIMEMSGPACK = binding.MIMEMSGPACK
MIMEMSGPACK2 = binding.MIMEMSGPACK2
)
var contentType = []string{"application/msgpack; charset=utf-8"}
func init() {
binding.Register(MIMEMSGPACK, Binding)
binding.Register(MIMEMSGPACK2, Binding)
factory := func(data any) render.Render { return renderer{Data: data} }
render.Register(MIMEMSGPACK, factory)
render.Register(MIMEMSGPACK2, factory)
}
// renderer implements render.Render for MessagePack responses.
type renderer struct {
Data any
}
var _ render.Render = renderer{}
// Render encodes the data as MessagePack and writes it with the MessagePack
// content type.
func (r renderer) Render(w http.ResponseWriter) error {
return WriteMsgPack(w, r.Data)
}
// WriteContentType writes the MessagePack content type.
func (r renderer) WriteContentType(w http.ResponseWriter) {
writeContentType(w, contentType)
}
// Render writes obj to the response as MessagePack with status code. It is the
// drop-in replacement for the former c.MsgPack(code, obj).
func Render(c *gin.Context, code int, obj any) {
c.Render(code, renderer{Data: obj})
}
// WriteMsgPack writes the MessagePack content type and encodes obj to w.
func WriteMsgPack(w http.ResponseWriter, obj any) error {
writeContentType(w, contentType)
var mh codec.MsgpackHandle
return codec.NewEncoder(w, &mh).Encode(obj)
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
// Binding decodes MessagePack request bodies. It can be passed to
// c.ShouldBindWith / c.MustBindWith.
var Binding binding.BindingBody = msgpackBinding{}
type msgpackBinding struct{}
func (msgpackBinding) Name() string {
return "msgpack"
}
func (msgpackBinding) Bind(req *http.Request, obj any) error {
return decodeMsgPack(req.Body, obj)
}
func (msgpackBinding) BindBody(body []byte, obj any) error {
return decodeMsgPack(bytes.NewReader(body), obj)
}
func decodeMsgPack(r io.Reader, obj any) error {
cdc := new(codec.MsgpackHandle)
if err := codec.NewDecoder(r, cdc).Decode(&obj); err != nil {
return err
}
return binding.Validate(obj)
}
// Bind binds the MessagePack request body to obj, aborting with HTTP 400 on
// error. It replaces the former c.BindWith(obj, binding.MsgPack).
func Bind(c *gin.Context, obj any) error {
return c.MustBindWith(obj, Binding)
}
// ShouldBind binds the MessagePack request body to obj without aborting.
func ShouldBind(c *gin.Context, obj any) error {
return c.ShouldBindWith(obj, Binding)
}

View File

@ -0,0 +1,80 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package msgpack
import (
"bytes"
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
type payload struct {
Foo string `msgpack:"foo" json:"foo"`
Bar string `msgpack:"bar" json:"bar" binding:"required"`
}
func decode(t *testing.T, b []byte) payload {
t.Helper()
var out payload
var mh codec.MsgpackHandle
require.NoError(t, codec.NewDecoder(bytes.NewReader(b), &mh).Decode(&out))
return out
}
func TestRender(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
got := decode(t, w.Body.Bytes())
assert.Equal(t, "foo", got.Foo)
assert.Equal(t, "bar", got.Bar)
}
func TestBinding(t *testing.T) {
var mh codec.MsgpackHandle
var buf bytes.Buffer
require.NoError(t, codec.NewEncoder(&buf, &mh).Encode(payload{Foo: "foo", Bar: "bar"}))
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewReader(buf.Bytes()))
var out payload
require.NoError(t, Binding.Bind(req, &out))
assert.Equal(t, "foo", out.Foo)
assert.Equal(t, "bar", out.Bar)
assert.Equal(t, "msgpack", Binding.Name())
}
func TestBindingValidation(t *testing.T) {
var mh codec.MsgpackHandle
var buf bytes.Buffer
require.NoError(t, codec.NewEncoder(&buf, &mh).Encode(payload{Foo: "foo"})) // Bar missing
var out payload
err := Binding.BindBody(buf.Bytes(), &out)
require.Error(t, err) // binding:"required" on Bar must fire
}
func TestRegistration(t *testing.T) {
// init() must have wired both content types into the registries.
for _, ct := range []string{MIMEMSGPACK, MIMEMSGPACK2} {
assert.Equal(t, Binding, binding.Default(http.MethodPost, ct), ct)
r, ok := render.Negotiate(ct, payload{Foo: "x"})
assert.True(t, ok, ct)
assert.NotNil(t, r)
}
}

View File

@ -1,36 +0,0 @@
// Copyright 2018 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http"
"google.golang.org/protobuf/proto"
)
// ProtoBuf contains the given interface object.
type ProtoBuf struct {
Data any
}
var protobufContentType = []string{"application/x-protobuf"}
// Render (ProtoBuf) marshals the given interface object and writes data with custom ContentType.
func (r ProtoBuf) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := proto.Marshal(r.Data.(proto.Message))
if err != nil {
return err
}
_, err = w.Write(bytes)
return err
}
// WriteContentType (ProtoBuf) writes ProtoBuf ContentType.
func (r ProtoBuf) WriteContentType(w http.ResponseWriter) {
writeContentType(w, protobufContentType)
}

123
render/protobuf/protobuf.go Normal file
View File

@ -0,0 +1,123 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// Package protobuf provides optional Protocol Buffers rendering and binding for
// gin.
//
// ProtoBuf support is no longer compiled into the core gin module. Import this
// package to opt back in:
//
// import (
// "github.com/gin-gonic/gin"
// "github.com/gin-gonic/gin/render/protobuf"
// )
//
// protobuf.Render(c, http.StatusOK, msg) // write a ProtoBuf response
// protobuf.ShouldBind(c, msg) // decode a ProtoBuf request body
//
// Importing the package registers the binding and renderer for the
// "application/x-protobuf" content type so that the content-type negotiation
// done by c.ShouldBind and c.Negotiate keeps working.
package protobuf
import (
"errors"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"google.golang.org/protobuf/proto"
)
// MIMEPROTOBUF is the content type handled by this package.
const MIMEPROTOBUF = binding.MIMEPROTOBUF
var contentType = []string{"application/x-protobuf"}
func init() {
binding.Register(MIMEPROTOBUF, Binding)
render.Register(MIMEPROTOBUF, func(data any) render.Render { return renderer{Data: data} })
}
// renderer implements render.Render for ProtoBuf responses.
type renderer struct {
Data any
}
var _ render.Render = renderer{}
// Render marshals the data as ProtoBuf and writes it with the ProtoBuf content
// type. The data must implement proto.Message.
func (r renderer) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := proto.Marshal(r.Data.(proto.Message))
if err != nil {
return err
}
_, err = w.Write(bytes)
return err
}
// WriteContentType writes the ProtoBuf content type.
func (r renderer) WriteContentType(w http.ResponseWriter) {
writeContentType(w, contentType)
}
// Render writes obj to the response as ProtoBuf with status code. It is the
// drop-in replacement for the former c.ProtoBuf(code, obj).
func Render(c *gin.Context, code int, obj any) {
c.Render(code, renderer{Data: obj})
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
// Binding decodes ProtoBuf request bodies. It can be passed to
// c.ShouldBindWith / c.MustBindWith.
var Binding binding.BindingBody = protobufBinding{}
type protobufBinding struct{}
func (protobufBinding) Name() string {
return "protobuf"
}
func (b protobufBinding) Bind(req *http.Request, obj any) error {
buf, err := io.ReadAll(req.Body)
if err != nil {
return err
}
return b.BindBody(buf, obj)
}
func (protobufBinding) BindBody(body []byte, obj any) error {
msg, ok := obj.(proto.Message)
if !ok {
return errors.New("obj is not ProtoMessage")
}
if err := proto.Unmarshal(body, msg); err != nil {
return err
}
// Here it's same to return binding.Validate(obj), but until now we can't
// add `binding:""` to the struct which is automatically generated by
// gen-proto.
return nil
}
// Bind binds the ProtoBuf request body to obj, aborting with HTTP 400 on error.
func Bind(c *gin.Context, obj any) error {
return c.MustBindWith(obj, Binding)
}
// ShouldBind binds the ProtoBuf request body to obj without aborting.
func ShouldBind(c *gin.Context, obj any) error {
return c.ShouldBindWith(obj, Binding)
}

View File

@ -0,0 +1,65 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package protobuf
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
)
func sample() *protoexample.Test {
return &protoexample.Test{
Label: proto.String("yes"),
Reps: []int64{1, 2, 3},
}
}
func TestRender(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
Render(c, http.StatusOK, sample())
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
var got protoexample.Test
require.NoError(t, proto.Unmarshal(w.Body.Bytes(), &got))
assert.Equal(t, "yes", got.GetLabel())
assert.Equal(t, []int64{1, 2, 3}, got.GetReps())
}
func TestBinding(t *testing.T) {
body, err := proto.Marshal(sample())
require.NoError(t, err)
var out protoexample.Test
require.NoError(t, Binding.BindBody(body, &out))
assert.Equal(t, "yes", out.GetLabel())
assert.Equal(t, "protobuf", Binding.Name())
}
func TestBindingNotProtoMessage(t *testing.T) {
err := Binding.BindBody([]byte("x"), &struct{ Foo string }{})
require.Error(t, err)
}
func TestRegistration(t *testing.T) {
assert.Equal(t, Binding, binding.Default(http.MethodPost, MIMEPROTOBUF))
r, ok := render.Negotiate(MIMEPROTOBUF, sample())
assert.True(t, ok)
assert.NotNil(t, r)
}

32
render/registry.go Normal file
View File

@ -0,0 +1,32 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
// Factory builds a Render for the given data. Optional format subpackages
// (github.com/gin-gonic/gin/render/<format>) register one per content type so
// that Context.Negotiate can produce their Render without the core importing
// the underlying codec library.
type Factory func(data any) Render
// registry maps a content type to its Factory. It is only written from init()
// functions before main runs, so it needs no synchronization.
var registry = map[string]Factory{}
// Register associates a Factory with a content type. It is intended to be
// called from an init() function in a format subpackage.
func Register(contentType string, factory Factory) {
registry[contentType] = factory
}
// Negotiate returns a Render for the content type and data when a Factory has
// been registered for it (i.e. the matching format subpackage was imported).
// The boolean reports whether a Factory was found.
func Negotiate(contentType string, data any) (Render, bool) {
factory, ok := registry[contentType]
if !ok {
return nil, false
}
return factory(data), true
}

View File

@ -26,11 +26,8 @@ var (
_ Render = (*HTML)(nil)
_ HTMLRender = (*HTMLDebug)(nil)
_ HTMLRender = (*HTMLProduction)(nil)
_ Render = (*YAML)(nil)
_ Render = (*Reader)(nil)
_ Render = (*AsciiJSON)(nil)
_ Render = (*ProtoBuf)(nil)
_ Render = (*TOML)(nil)
_ Render = (*PDF)(nil)
)

View File

@ -1,80 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
//go:build !nomsgpack
package render
import (
"errors"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/ugorji/go/codec"
)
func TestRenderMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
}
(MsgPack{data}).WriteContentType(w)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
err := (MsgPack{data}).Render(w)
require.NoError(t, err)
var decoded map[string]any
var mh codec.MsgpackHandle
mh.RawToString = true
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
require.NoError(t, err)
assert.Equal(t, data, decoded)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestWriteMsgPack(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
"num": 42,
}
err := WriteMsgPack(w, data)
require.NoError(t, err)
assert.Equal(t, "application/msgpack; charset=utf-8", w.Header().Get("Content-Type"))
var decoded map[string]any
var mh codec.MsgpackHandle
mh.RawToString = true
err = codec.NewDecoderBytes(w.Body.Bytes(), &mh).Decode(&decoded)
require.NoError(t, err)
assert.Len(t, decoded, 2)
assert.Equal(t, "bar", decoded["foo"])
assert.EqualValues(t, 42, decoded["num"])
}
type failWriter struct {
*httptest.ResponseRecorder
}
func (w *failWriter) Write(data []byte) (int, error) {
return 0, errors.New("write error")
}
func TestRenderMsgPackError(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
}
err := (MsgPack{data}).Render(&failWriter{w})
require.Error(t, err)
assert.Contains(t, err.Error(), "write error")
}

View File

@ -9,18 +9,14 @@ import (
"errors"
"html/template"
"io"
"net"
"net/http"
"net/http/httptest"
"strconv"
"strings"
"testing"
testdata "github.com/gin-gonic/gin/testdata/protoexample"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/proto"
)
func TestRenderJSON(t *testing.T) {
@ -305,142 +301,6 @@ func (h xmlmap) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
return e.EncodeToken(xml.EndElement{Name: start.Name})
}
func TestRenderYAML(t *testing.T) {
w := httptest.NewRecorder()
data := `
a : Easy!
b:
c: 2
d: [3, 4]
`
(YAML{data}).WriteContentType(w)
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
err := (YAML{data}).Render(w)
require.NoError(t, err)
// With github.com/goccy/go-yaml, the output format is different from gopkg.in/yaml.v3
// We're checking that the output contains the expected data, not the exact formatting
output := w.Body.String()
assert.Contains(t, output, "a : Easy!")
assert.Contains(t, output, "b:")
assert.Contains(t, output, "c: 2")
assert.Contains(t, output, "d: [3, 4]")
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
}
type fail struct{}
// Hook MarshalYAML
func (ft *fail) MarshalYAML() (any, error) {
return nil, errors.New("fail")
}
func TestRenderYAMLFail(t *testing.T) {
w := httptest.NewRecorder()
err := (YAML{&fail{}}).Render(w)
require.Error(t, err)
}
func TestRenderTOML(t *testing.T) {
w := httptest.NewRecorder()
data := map[string]any{
"foo": "bar",
"html": "<b>",
}
(TOML{data}).WriteContentType(w)
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
err := (TOML{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, "foo = 'bar'\nhtml = '<b>'\n", w.Body.String())
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
}
func TestRenderTOMLFail(t *testing.T) {
w := httptest.NewRecorder()
err := (TOML{net.IPv4bcast}).Render(w)
require.Error(t, err)
}
// test Protobuf rendering
func TestRenderProtoBuf(t *testing.T) {
w := httptest.NewRecorder()
reps := []int64{int64(1), int64(2)}
label := "test"
data := &testdata.Test{
Label: &label,
Reps: reps,
}
(ProtoBuf{data}).WriteContentType(w)
protoData, err := proto.Marshal(data)
require.NoError(t, err)
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
err = (ProtoBuf{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, string(protoData), w.Body.String())
assert.Equal(t, "application/x-protobuf", w.Header().Get("Content-Type"))
}
func TestRenderProtoBufFail(t *testing.T) {
w := httptest.NewRecorder()
data := &testdata.Test{}
err := (ProtoBuf{data}).Render(w)
require.Error(t, err)
}
func TestRenderBSON(t *testing.T) {
w := httptest.NewRecorder()
reps := []int64{int64(1), int64(2)}
type mystruct struct {
Label string
Reps []int64
}
data := &mystruct{
Label: "test",
Reps: reps,
}
(BSON{data}).WriteContentType(w)
bsonData, err := bson.Marshal(data)
require.NoError(t, err)
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
err = (BSON{data}).Render(w)
require.NoError(t, err)
assert.Equal(t, bsonData, w.Body.Bytes())
assert.Equal(t, "application/bson", w.Header().Get("Content-Type"))
}
func TestRenderBSONError(t *testing.T) {
w := httptest.NewRecorder()
data := make(chan int)
err := (BSON{data}).Render(w)
require.Error(t, err)
}
func TestRenderBSONWriteError(t *testing.T) {
type testStruct struct {
Value string
}
data := &testStruct{Value: "test"}
ew := &errorWriter{
ErrThreshold: 1,
ResponseRecorder: httptest.NewRecorder(),
}
err := (BSON{data}).Render(ew)
require.Error(t, err)
assert.Equal(t, "write error", err.Error())
}
func TestRenderXML(t *testing.T) {
w := httptest.NewRecorder()
data := xmlmap{

View File

@ -1,36 +0,0 @@
// Copyright 2022 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http"
"github.com/pelletier/go-toml/v2"
)
// TOML contains the given interface object.
type TOML struct {
Data any
}
var tomlContentType = []string{"application/toml; charset=utf-8"}
// Render (TOML) marshals the given interface object and writes data with custom ContentType.
func (r TOML) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := toml.Marshal(r.Data)
if err != nil {
return err
}
_, err = w.Write(bytes)
return err
}
// WriteContentType (TOML) writes TOML ContentType for response.
func (r TOML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, tomlContentType)
}

117
render/toml/toml.go Normal file
View File

@ -0,0 +1,117 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// Package toml provides optional TOML rendering and binding for gin.
//
// TOML support is no longer compiled into the core gin module. Import this
// package to opt back in:
//
// import (
// "github.com/gin-gonic/gin"
// "github.com/gin-gonic/gin/render/toml"
// )
//
// toml.Render(c, http.StatusOK, obj) // write a TOML response
// toml.ShouldBind(c, &obj) // decode a TOML request body
//
// Importing the package registers the binding and renderer for the
// "application/toml" content type so that the content-type negotiation done by
// c.ShouldBind and c.Negotiate keeps working.
package toml
import (
"bytes"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/pelletier/go-toml/v2"
)
// MIMETOML is the content type handled by this package.
const MIMETOML = binding.MIMETOML
var contentType = []string{"application/toml; charset=utf-8"}
func init() {
binding.Register(MIMETOML, Binding)
render.Register(MIMETOML, func(data any) render.Render { return renderer{Data: data} })
}
// renderer implements render.Render for TOML responses.
type renderer struct {
Data any
}
var _ render.Render = renderer{}
// Render marshals the data as TOML and writes it with the TOML content type.
func (r renderer) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := toml.Marshal(r.Data)
if err != nil {
return err
}
_, err = w.Write(bytes)
return err
}
// WriteContentType writes the TOML content type.
func (r renderer) WriteContentType(w http.ResponseWriter) {
writeContentType(w, contentType)
}
// Render writes obj to the response as TOML with status code. It is the
// drop-in replacement for the former c.TOML(code, obj).
func Render(c *gin.Context, code int, obj any) {
c.Render(code, renderer{Data: obj})
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
// Binding decodes TOML request bodies. It can be passed to
// c.ShouldBindWith / c.MustBindWith.
var Binding binding.BindingBody = tomlBinding{}
type tomlBinding struct{}
func (tomlBinding) Name() string {
return "toml"
}
func (tomlBinding) Bind(req *http.Request, obj any) error {
return decodeToml(req.Body, obj)
}
func (tomlBinding) BindBody(body []byte, obj any) error {
return decodeToml(bytes.NewReader(body), obj)
}
func decodeToml(r io.Reader, obj any) error {
decoder := toml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return binding.Validate(obj)
}
// Bind binds the TOML request body to obj, aborting with HTTP 400 on error.
// It replaces the former c.BindTOML(obj).
func Bind(c *gin.Context, obj any) error {
return c.MustBindWith(obj, Binding)
}
// ShouldBind binds the TOML request body to obj without aborting. It replaces
// the former c.ShouldBindTOML(obj).
func ShouldBind(c *gin.Context, obj any) error {
return c.ShouldBindWith(obj, Binding)
}

59
render/toml/toml_test.go Normal file
View File

@ -0,0 +1,59 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package toml
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type payload struct {
Foo string `toml:"foo"`
Bar string `toml:"bar" binding:"required"`
}
func TestRender(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/toml; charset=utf-8", w.Header().Get("Content-Type"))
assert.Contains(t, w.Body.String(), "foo")
assert.Contains(t, w.Body.String(), "bar")
}
func TestBinding(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo = 'foo'\nbar = 'bar'\n"))
var out payload
require.NoError(t, Binding.Bind(req, &out))
assert.Equal(t, "foo", out.Foo)
assert.Equal(t, "bar", out.Bar)
assert.Equal(t, "toml", Binding.Name())
}
func TestBindingValidation(t *testing.T) {
var out payload
err := Binding.BindBody([]byte("foo = 'foo'\n"), &out) // Bar missing
require.Error(t, err)
}
func TestRegistration(t *testing.T) {
assert.Equal(t, Binding, binding.Default(http.MethodPost, MIMETOML))
r, ok := render.Negotiate(MIMETOML, payload{Foo: "x"})
assert.True(t, ok)
assert.NotNil(t, r)
}

View File

@ -1,36 +0,0 @@
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package render
import (
"net/http"
"github.com/goccy/go-yaml"
)
// YAML contains the given interface object.
type YAML struct {
Data any
}
var yamlContentType = []string{"application/yaml; charset=utf-8"}
// Render (YAML) marshals the given interface object and writes data with custom ContentType.
func (r YAML) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := yaml.Marshal(r.Data)
if err != nil {
return err
}
_, err = w.Write(bytes)
return err
}
// WriteContentType (YAML) writes YAML ContentType for response.
func (r YAML) WriteContentType(w http.ResponseWriter) {
writeContentType(w, yamlContentType)
}

123
render/yaml/yaml.go Normal file
View File

@ -0,0 +1,123 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
// Package yaml provides optional YAML rendering and binding for gin.
//
// YAML support is no longer compiled into the core gin module. Import this
// package to opt back in:
//
// import (
// "github.com/gin-gonic/gin"
// "github.com/gin-gonic/gin/render/yaml"
// )
//
// yaml.Render(c, http.StatusOK, obj) // write a YAML response
// yaml.ShouldBind(c, &obj) // decode a YAML request body
//
// Importing the package registers the binding and renderer for the
// "application/x-yaml" and "application/yaml" content types so that the
// content-type negotiation done by c.ShouldBind and c.Negotiate keeps working.
package yaml
import (
"bytes"
"io"
"net/http"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/goccy/go-yaml"
)
// Content types handled by this package.
const (
MIMEYAML = binding.MIMEYAML
MIMEYAML2 = binding.MIMEYAML2
)
var contentType = []string{"application/yaml; charset=utf-8"}
func init() {
binding.Register(MIMEYAML, Binding)
binding.Register(MIMEYAML2, Binding)
factory := func(data any) render.Render { return renderer{Data: data} }
render.Register(MIMEYAML, factory)
render.Register(MIMEYAML2, factory)
}
// renderer implements render.Render for YAML responses.
type renderer struct {
Data any
}
var _ render.Render = renderer{}
// Render marshals the data as YAML and writes it with the YAML content type.
func (r renderer) Render(w http.ResponseWriter) error {
r.WriteContentType(w)
bytes, err := yaml.Marshal(r.Data)
if err != nil {
return err
}
_, err = w.Write(bytes)
return err
}
// WriteContentType writes the YAML content type.
func (r renderer) WriteContentType(w http.ResponseWriter) {
writeContentType(w, contentType)
}
// Render writes obj to the response as YAML with status code. It is the
// drop-in replacement for the former c.YAML(code, obj).
func Render(c *gin.Context, code int, obj any) {
c.Render(code, renderer{Data: obj})
}
func writeContentType(w http.ResponseWriter, value []string) {
header := w.Header()
if val := header["Content-Type"]; len(val) == 0 {
header["Content-Type"] = value
}
}
// Binding decodes YAML request bodies. It can be passed to
// c.ShouldBindWith / c.MustBindWith.
var Binding binding.BindingBody = yamlBinding{}
type yamlBinding struct{}
func (yamlBinding) Name() string {
return "yaml"
}
func (yamlBinding) Bind(req *http.Request, obj any) error {
return decodeYAML(req.Body, obj)
}
func (yamlBinding) BindBody(body []byte, obj any) error {
return decodeYAML(bytes.NewReader(body), obj)
}
func decodeYAML(r io.Reader, obj any) error {
decoder := yaml.NewDecoder(r)
if err := decoder.Decode(obj); err != nil {
return err
}
return binding.Validate(obj)
}
// Bind binds the YAML request body to obj, aborting with HTTP 400 on error.
// It replaces the former c.BindYAML(obj).
func Bind(c *gin.Context, obj any) error {
return c.MustBindWith(obj, Binding)
}
// ShouldBind binds the YAML request body to obj without aborting. It replaces
// the former c.ShouldBindYAML(obj).
func ShouldBind(c *gin.Context, obj any) error {
return c.ShouldBindWith(obj, Binding)
}

61
render/yaml/yaml_test.go Normal file
View File

@ -0,0 +1,61 @@
// Copyright 2025 Gin Core Team. All rights reserved.
// Use of this source code is governed by a MIT style
// license that can be found in the LICENSE file.
package yaml
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/gin-gonic/gin/render"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type payload struct {
Foo string `yaml:"foo"`
Bar string `yaml:"bar" binding:"required"`
}
func TestRender(t *testing.T) {
gin.SetMode(gin.TestMode)
w := httptest.NewRecorder()
c, _ := gin.CreateTestContext(w)
Render(c, http.StatusOK, payload{Foo: "foo", Bar: "bar"})
assert.Equal(t, http.StatusOK, w.Code)
assert.Equal(t, "application/yaml; charset=utf-8", w.Header().Get("Content-Type"))
assert.Contains(t, w.Body.String(), "foo: foo")
assert.Contains(t, w.Body.String(), "bar: bar")
}
func TestBinding(t *testing.T) {
req := httptest.NewRequest(http.MethodPost, "/", strings.NewReader("foo: foo\nbar: bar\n"))
var out payload
require.NoError(t, Binding.Bind(req, &out))
assert.Equal(t, "foo", out.Foo)
assert.Equal(t, "bar", out.Bar)
assert.Equal(t, "yaml", Binding.Name())
}
func TestBindingValidation(t *testing.T) {
var out payload
err := Binding.BindBody([]byte("foo: foo\n"), &out) // Bar missing
require.Error(t, err)
}
func TestRegistration(t *testing.T) {
for _, ct := range []string{MIMEYAML, MIMEYAML2} {
assert.Equal(t, Binding, binding.Default(http.MethodPost, ct), ct)
r, ok := render.Negotiate(ct, payload{Foo: "x"})
assert.True(t, ok, ct)
assert.NotNil(t, r)
}
}