pedantigo v2: check request bodies in Echo and Gin

pedantigo v2: check request bodies in Echo and Gin

by Tushar Dwivedi
Ask AI about this page

pedantigo v2 is the second major version of the MIT-licensed Go validation library. It keeps the validation engine, changes the default struct tag from pedantigo to validate, and adds plugins. The first plugins are binders for the Echo and Gin web frameworks.

What changed in v2

The table compares version 1 and version 2 of the library, area by area.

Areav1v2
Module pathgithub.com/SmrutAI/pedantigogithub.com/SmrutAI/pedantigo/v2
Importgithub.com/SmrutAI/pedantigogithub.com/SmrutAI/pedantigo/v2/validator
Default tagpedantigovalidate
Options typeValidatorOptionsOptions, with new TagName and MaxRecursionDepth fields
Web frameworksNoneEcho and Gin plugins, each its own Go module

All the code lives in the validator package. Install it with:

go get github.com/SmrutAI/pedantigo/v2/validator

Version 1 stays frozen at v1.1.4.

Why the default tag changed

The validate tag matches go-playground/validator and swaggo/swag. Structs written for that ecosystem now work with pedantigo without edits.

Migrate from v1

  1. Update the import to github.com/SmrutAI/pedantigo/v2/validator.
  2. Replace the pedantigo. qualifier in your code with validator..
  3. Rename the struct tags to validate, or call validator.SetTagName("pedantigo") before you create any validator.
  4. Audit your tags in both directions.

Step 4 matters because the failure is silent. Version 2 ignores a leftover pedantigo:"..." tag, so the field becomes unconstrained, and no error appears. An existing validate:"..." tag that pedantigo never read before is now enforced.

Do not rename the literal word pedantigo in tags with a blind find and replace. Rename the ValidatorOptions type to Options by hand.

Register a type once

The plugins need to find your validator by type. Register each request type once.

type CreateUser struct {
	Name  string `json:"name" validate:"required,min=1,max=100"`
	Email string `json:"email" validate:"required,email"`
}

var _ = validator.Register(validator.New[CreateUser]())

A second Register call for the same type panics. New alone does not make the type findable.

Echo plugin

The Echo plugin is the module github.com/SmrutAI/pedantigo/plugins/web/pedantigoecho/v2. It provides a binder that replaces Echo's default.

package main

import (
	"net/http"

	"github.com/labstack/echo/v4"

	"github.com/SmrutAI/pedantigo/plugins/web/pedantigoecho/v2"
	"github.com/SmrutAI/pedantigo/v2/validator"
)

type CreateUser struct {
	Name  string `json:"name" validate:"required,min=1,max=100"`
	Email string `json:"email" validate:"required,email"`
}

var _ = validator.Register(validator.New[CreateUser]())

func create(c echo.Context) error {
	var req CreateUser
	if err := c.Bind(&req); err != nil {
		return err // *echo.HTTPError, status 400
	}
	return c.JSON(http.StatusCreated, req)
}

func main() {
	e := echo.New()
	e.Binder = pedantigoecho.NewBinder()
	e.POST("/users", create)
	e.Logger.Fatal(e.Start(":8080"))
}

The binder works in four steps:

  1. It binds path parameters first.
  2. For GET, DELETE and HEAD, it uses Echo's default binder for query parameters. pedantigo does not run.
  3. For POST, PUT and PATCH, it reads the body and validates it. A failure returns an *echo.HTTPError with status 400.
  4. An empty body returns without validation.

A body with no email, or with "email":"not-an-email", returns the 400 error. A type that you did not register panics.

Gin plugin

The Gin plugin is the module github.com/SmrutAI/pedantigo/plugins/web/pedantigogin/v2. It installs two global hooks, a JSON codec and a validator, when you call NewBinder.

package main

import (
	"net/http"

	"github.com/gin-gonic/gin"

	"github.com/SmrutAI/pedantigo/plugins/web/pedantigogin/v2"
	"github.com/SmrutAI/pedantigo/v2/validator"
)

type CreateUser struct {
	Name  string `json:"name" binding:"required,min=1,max=100"`
	Email string `json:"email" binding:"required,email"`
}

var _ = validator.Register(validator.New[CreateUser](
	validator.Options{TagName: "binding", StrictMissingFields: true},
))

func create(c *gin.Context) {
	var req CreateUser
	if err := c.ShouldBindJSON(&req); err != nil {
		if ve, ok := pedantigogin.AsValidationError(err); ok {
			c.JSON(http.StatusBadRequest, ve.Errors)
			return
		}
		c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
		return
	}
	c.JSON(http.StatusCreated, req)
}

func main() {
	pedantigogin.NewBinder()
	r := gin.Default()
	r.POST("/users", create)
	_ = r.Run(":8080")
}

Gin reads the binding tag, so the options set TagName: "binding". The StrictMissingFields: true setting matters. Passing Options{TagName: "binding"} alone replaces the defaults with a zero struct. Missing-field detection then turns off, and a missing required key passes.

The Gin plugin has two limits:

  • Only JSON bodies get missing-field detection and defaults. For query, form, header and URI binding, only value constraints run, and a missing required field passes.
  • The module needs Go 1.25 or later. The Echo plugin needs Go 1.23 or later.

Versions to avoid

The go.mod file retracts two early v2 tags:

VersionProblem
v2.0.0Published with a stale layout that the module proxy cached
v2.1.0The plugin module paths broke Go's major-version suffix rule, so its install instructions failed

Use v2.1.3 or later. The first working Echo and Gin plugin tags are v2.1.1.

Learn more

The v1 to v2 migration guide has the full steps. The Echo and Gin plugin pages cover the binders. The source is on GitHub.

Related posts

Comments