pedantigo: Pydantic-inspired Go validation and LLM JSON Schema

pedantigo: Pydantic-inspired Go validation and LLM JSON Schema

by Tushar Dwivedi
Ask AI about this page

pedantigo is an MIT-licensed Go library that unmarshals JSON into structs while it enforces constraints declared in struct tags. It also generates JSON Schema from the same structs. The design follows Pydantic, the Python validation library.

This post covers version 1.1.0.

The problem

Go services read JSON from outside sources: API clients, AI agents and user input. Plain json.Unmarshal checks syntax and types. It does not check that an email is an email or that an age is at least 18. You then write that logic by hand for each struct.

pedantigo keeps the rules next to the fields, and one call applies them.

Install

go get github.com/SmrutAI/pedantigo

The module needs Go 1.21 or later. The import path is case sensitive, so write SmrutAI with capital letters.

Check input and read the errors

A constraint is a rule in a struct tag, such as min=18. The library has more than 100 built-in constraints. This sample uses three of them.

package main

import (
	"errors"
	"fmt"

	"github.com/SmrutAI/pedantigo"
)

type User struct {
	Email string `json:"email" pedantigo:"required,email"`
	Age   int    `json:"age" pedantigo:"min=18"`
}

func main() {
	data := []byte(`{"email":"notanemail","age":15}`)
	user, err := pedantigo.Unmarshal[User](data)
	var ve *pedantigo.ValidationError
	if errors.As(err, &ve) {
		for _, fe := range ve.Errors {
			fmt.Printf("%s [%s]: %s\n",
				fe.Field, fe.Code, fe.Message)
		}
	}
	fmt.Println(user != nil) // the struct still comes back
}

Unmarshal returns a *pedantigo.ValidationError that lists every failing field. For this input the messages are must be a valid email address (code INVALID_EMAIL) and must be at least 18 (code MIN_VALUE). The struct also comes back, so you can inspect what arrived.

Invalid JSON syntax is a different failure. It returns a plain error, not a ValidationError.

Two details trip up new users:

  • A value-constraint error names the Go struct field, such as Email. A missing required key reports the JSON key.
  • The library ignores a constraint name it does not know, without any error. Stick to documented names: min and max (length for strings, slices and maps; value for numbers), gt, gte, lt, lte, regexp and oneof.

Generate a schema for a tool call

LLM tool calling needs a JSON Schema for each tool's arguments. pedantigo builds one from the struct you already check input with.

package main

import (
	"fmt"

	"github.com/SmrutAI/pedantigo"
)

type User struct {
	Name  string `json:"name" pedantigo:"required,min=3"`
	Email string `json:"email" pedantigo:"required,email"`
	Age   int    `json:"age" pedantigo:"gte=18,lte=120"`
}

func main() {
	b, err := pedantigo.SchemaJSON[User]()
	if err != nil {
		panic(err)
	}
	fmt.Println(string(b))
}

The schema has type: object. The required list holds the required fields. min=3 on the name becomes minLength: 3, and email becomes format: email. On numbers, gte and lte map to minimum and maximum, while gt and lt map to exclusiveMinimum and exclusiveMaximum. SchemaJSON inlines nested structs instead of using $ref. SchemaJSONOpenAPI uses $ref into $defs.

Check a streamed answer

LLM APIs stream a JSON answer in chunks. NewStreamParser collects the chunks and validates the result once the JSON is complete.

package main

import (
	"fmt"

	"github.com/SmrutAI/pedantigo"
)

type StreamUser struct {
	Name  string `json:"name" pedantigo:"required"`
	Email string `json:"email" pedantigo:"email"`
	Age   int    `json:"age" pedantigo:"min=0"`
}

func main() {
	p := pedantigo.NewStreamParser[StreamUser]()
	chunks := []string{
		`{"name":"Jo`,
		`hn","email":"not-an-email","age":25}`,
	}
	for _, c := range chunks {
		obj, st, err := p.Feed([]byte(c))
		if !st.IsComplete {
			fmt.Println("waiting,", st.BytesReceived, "bytes")
			continue
		}
		fmt.Println(obj.Name, err) // obj is set even when err is not
		p.Reset()
	}
}

The parser does not repair broken JSON. Feed returns a nil struct and no error until the buffer holds valid JSON. A complete payload that breaks a rule returns the struct and an error, and the error text names the field.

What to read next

Related posts

Comments