This episode reviews Gin's latest stable features: v1.11 with ShouldBindBodyWithPlain, experimental HTTP/3, and deprecated API cleanup; v1.12 with encoding.UnmarshalText, Protobuf, OptionFunc, and the convenience brought by engine.With.

Since episode 0 we've been using Gin v1.12.0, but we've never reviewed what changed in the last two releases. This episode 20 dissects Gin's latest stable features: releases v1.11.0 (September 2025) and v1.12.0 (February 2026). You'll see how binding was strengthened, what was cleaned up from the old API, and which features are most useful for your projects.
Understanding the release direction matters because it affects upgrade decisions. The good news: there are no major breaking changes — upgrading from v1.10 to v1.12 is usually just go get. What changed is added API that makes code more expressive and faster.
Gin v1.11 introduced BindPlain and ShouldBindBodyWithPlain for reading raw text bodies, plus unixMilli and unixMicro support for time binding:
type Message struct {
Content string
}
func webhookHandler(c *gin.Context) {
var msg Message
if err := c.ShouldBindBodyWithPlain(&msg, binding.String); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(200, gin.H{"content": msg.Content})
}c.ShouldBindBodyWithPlain(&msg, binding.String) maps a plain text body to a string field. This is useful for webhooks that send text payloads without Content-Type: application/json. This version also marked old deprecated functions for cleanup and added RunQUIC for HTTP/3, which is still experimental.
The flagship v1.12 feature: URI and query binding now honors encoding.UnmarshalText. That means you can create custom types that parse their own values:
type Clock struct {
time.Time
}
func (c *Clock) UnmarshalText(text []byte) error {
t, err := time.Parse("15:04:05", string(text))
if err != nil {
return err
}
c.Time = t
return nil
}
type ScheduleRequest struct {
Start Clock `form:"start" binding:"required"`
}var req ScheduleRequest
if err := c.ShouldBindQuery(&req); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}c.ShouldBindQuery(&req) calls UnmarshalText on the Clock type when filling the start=08:30:00 query. Before v1.12, parsing special formats in a query had to be done manually after binding. Now the customization lives in the type itself, so the rule applies consistently across all routes.
The ShouldBindBodyWith* family now serves more formats:
c.ShouldBindBodyWithJSON(&data)
c.ShouldBindBodyWithXML(&data)
c.ShouldBindBodyWithYAML(&data)
c.ShouldBindBodyWithTOML(&data)c.ShouldBindBodyWithYAML(&data) reads a YAML body while storing a copy of the body in the context — so it can be called more than once in a single handler, unlike a plain ShouldBind. In v1.12, body handling for TOML and YAML was also fixed so it doesn't decode twice, and body binding for Protobuf is supported in content negotiation.
Since v1.10, gin.New and gin.Default accept OptionFunc — a function that modifies the engine:
func withLogger(logger *slog.Logger) gin.OptionFunc {
return func(e *gin.Engine) {
e.Use(slogMiddleware(logger))
}
}
r := gin.New(withLogger(appLogger))gin.New(withLogger(appLogger)) configures the engine at construction time, not after. The withLogger function returns an OptionFunc that installs middleware. This approach makes configuration composition modular and easy to test.
To apply options after the engine already exists, use engine.With:
r := gin.New()
r.With(withLogger(appLogger), withRecovery(appLogger))engine.With(withLogger(...)) returns an engine that's already configured, so it can be chained. This functional options pattern became even cleaner in v1.11 and v1.12 — you can chain logger, recovery, and other middleware in one expressive line.
Gin now has a Skipper type — a function that takes a context and returns a boolean — to decide whether middleware runs:
type Skipper func(c *gin.Context) bool
skipHealth := func(c *gin.Context) bool {
return c.Request.URL.Path == "/healthz"
}
r.Use(loggerMiddlewareWith(skipHealth))skipHealth keeps the health and metrics endpoints from flooding the access log — a pattern that pays off immediately after episode 18. In v1.12, the logger configuration also allows disabling query string output and color-coding latency so slow requests are easy to spot in the terminal.
To upgrade an existing project:
go get github.com/gin-gonic/gin@v1.12.0
go mod tidy
go build ./...go get github.com/gin-gonic/gin@v1.12.0 pulls the latest release. Since there are no breaking changes in the public API you use, go build ./... should succeed right away. Note that v1.12 requires a minimum of Go 1.25 — make sure your toolchain qualifies before upgrading.
Key takeaways:
ShouldBindBodyWithPlain and unixMilli/unixMicro time support.encoding.UnmarshalText for URI and query binding.ShouldBindBodyWith* now serves JSON, XML, YAML, and TOML consistently.OptionFunc and engine.With make engine configuration modular.Skipper lets middleware skip certain endpoints.In the next episode, episode 21, we'll dissect production-ready architecture — combining all the lessons into a production architecture: modular layout, graceful shutdown, env configuration, centralized logging, containerization with multi-stage Docker, a CI/CD pipeline, and zero-downtime deployment.