Go library for JSON Schema Draft 2020-12 validation with enhanced outputs and multilingual error messages
Go
227
248 commits
updated Sep 14, 2026
A high-performance JSON Schema validator for Go with direct struct validation, default-aware unmarshaling, a fluent constructor API, and support for Draft 2020-12, Draft 2019-09, Draft-07, Draft-06, and Draft-04.
schema.Validate(input) accepts raw JSON, maps, or Go structs.encoding/json.Number instead of being rounded through float64.schema.Unmarshal applies schema defaults; validation stays a separate step.Object, Prop, String, Required, and composition helpers.FromStruct.i18n subpackage, or implement the one-method Translator interface yourself.go get github.com/kaptinlin/jsonschema
Requires the Go version declared in go.mod.
package main
import (
"fmt"
"log"
"github.com/kaptinlin/jsonschema"
)
type User struct {
Name string `json:"name"`
Country string `json:"country"`
}
func main() {
schema, err := jsonschema.NewCompiler().Compile([]byte(`{
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"country": {"type": "string", "default": "US"}
},
"required": ["name"]
}`))
if err != nil {
log.Fatal(err)
}
data := []byte(`{"name":"Alice"}`)
result := schema.Validate(data)
if !result.IsValid() {
log.Fatalf("invalid payload: %v", result.Errors)
}
var user User
if err := schema.Unmarshal(&user, data); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", user)
}
schema.Validate is the main entry point. Pass raw JSON, a decoded map, or a Go struct — it dispatches internally.
result := schema.Validate([]byte(`{"name":"Alice"}`))
result = schema.Validate(map[string]any{"name": "Alice"})
result = schema.Validate(User{Name: "Alice"})
When the input type is known and you want to skip the dispatch and any extra conversion, call the type-specific method directly. They share semantics with Validate.
| Method | Use When |
|---|---|
ValidateJSON([]byte) | Hot paths handling raw JSON request bodies or stored documents |
ValidateMap(map[string]any) | Already-decoded JSON objects you do not want re-encoded |
ValidateStruct(any) | Go values, when you want to avoid a JSON round-trip |
The package's default JSON codec preserves every untyped number as
encoding/json.Number. Numeric constraints, enum, const, uniqueItems,
and numeric format callbacks compare values mathematically, including integers
beyond the exact range of float64.
import stdjson "encoding/json"
numberSchema := jsonschema.Number()
var value any
if err := numberSchema.Unmarshal(&value, []byte(`18446744073709551615`)); err != nil {
log.Fatal(err)
}
number := value.(stdjson.Number)
fmt.Println(number.String()) // 18446744073709551615
Direct Go inputs retain their native numeric types. Use jsonschema.NewRat in
numeric custom formats when the callback must accept both native Go numbers and
encoding/json.Number. A codec installed with WithDecoderJSON or
WithEncoderJSON replaces the corresponding default behavior, so the caller
owns its dynamic number types and precision guarantees.
When passing untyped values to a non-JSON serializer, adapt
encoding/json.Number at that serializer boundary. Do not convert through
float64; emit an exact target-format number or return an error.
Schemas are compiled using the dialect declared by $schema. When $schema is absent, the compiler defaults to Draft 2020-12.
compiler := jsonschema.NewCompiler()
compiler.SetDefaultDialect(jsonschema.Draft7) // for schemas without "$schema"
schema, err := compiler.Compile(schemaBytes)
Supported dialects are:
| Dialect | Constant |
|---|---|
| Draft 2020-12 | jsonschema.Draft202012 |
| Draft 2019-09 | jsonschema.Draft201909 |
| Draft-07 | jsonschema.Draft7 |
| Draft-06 | jsonschema.Draft6 |
| Draft-04 | jsonschema.Draft4 |
format remains annotation-only in the standard Draft 2020-12 dialect. Custom
dialects can require Format-Assertion, and callers can independently enable
best-effort assertion with SetAssertFormat(true).
Compile does not perform schema meta-validation by default; call
ValidateSchema when the schema document itself is untrusted.
result, err := compiler.ValidateSchema(schemaBytes)
if err != nil {
log.Fatal(err)
}
if !result.IsValid() {
log.Fatalf("invalid schema document: %v", result.Errors)
}
See docs/dialects.md for compatibility details.
Build schemas directly in Go when you want type-safe composition instead of raw JSON literals.
userSchema := jsonschema.Object(
jsonschema.Prop("name", jsonschema.String(jsonschema.MinLength(1))),
jsonschema.Prop("email", jsonschema.Email()),
jsonschema.Prop("age", jsonschema.Integer(jsonschema.Min(0))),
jsonschema.Required("name", "email"),
)
result := userSchema.Validate(map[string]any{
"name": "Alice",
"email": "alice@example.com",
"age": 30,
})
fmt.Println(result.IsValid())
The constructor layer also includes composition helpers such as OneOf, AnyOf, AllOf, Not, and If(...).Then(...).Else(...).
Generate schemas from Go types at runtime with FromStruct, or use schemagen for generated code.
type Signup struct {
Name string `jsonschema:"required,minLength=2,maxLength=50"`
Email string `jsonschema:"required,format=email"`
Age int `jsonschema:"minimum=18"`
}
schema, err := jsonschema.FromStruct[Signup]()
if err != nil {
log.Fatal(err)
}
fmt.Println(schema.Validate(map[string]any{
"name": "Alice",
"email": "alice@example.com",
"age": 28,
}).IsValid())
Install the generator when you want compile-time helpers:
go install github.com/kaptinlin/jsonschema/cmd/schemagen@latest
schemagen
Use FromStructWithOptions when you need a custom tag name, schema version, required-field ordering, or schema-level properties.
Format validation is annotation-only by default. Enable best-effort assertion explicitly and register your own validators when application policy requires it. Schemas using a custom Format-Assertion dialect assert formats automatically.
compiler := jsonschema.NewCompiler().SetAssertFormat(true)
compiler.RegisterFormat("customer-id", func(v any) bool {
s, ok := v.(string)
return ok && strings.HasPrefix(s, "CUST-")
}, "string")
Register default functions on a compiler, then unmarshal validated data through schemas that use those defaults.
compiler := jsonschema.NewCompiler()
compiler.RegisterDefaultFunc("now", jsonschema.DefaultNowFunc)
CompileBatch for interdependent resources. Compilation rejects missing
references and duplicate resource definitions; use Compiler.Schema to reuse
a published definition. Concurrent loading of a shared dependency reuses the
first published resource. Later compilation never rebinds an existing graph.MarshalJSON followed by Compile once; direct
validation of mutable builders checks the entire graph for unresolved references.SetPreserveExtra(true) when tools need to keep non-standard extension keywords in Schema.Extra.RegisterLoader. http and https ship pre-registered with a 10s timeout. If your schemas come from untrusted sources, replace or remove those loaders so external $ref resolution is gated by your own host/size/timeout policy.The root package knows nothing about translation frameworks — pure validation users pay zero Intl compile, link, and binary cost. Localization lives in the optional i18n subpackage:
import "github.com/kaptinlin/jsonschema/i18n"
zh, err := i18n.New("zh-Hans") // one translator per locale
if err != nil {
log.Fatal(err)
}
localized := result.ToLocalizedList(zh)
fmt.Println(localized.Valid)
for path, message := range result.LocalizedDetailedErrors(zh) {
fmt.Printf("%s: %s\n", path, message)
}
Built-in locales: en, de-DE, es-ES, fr-FR, ja-JP, ko-KR, pt-BR, zh-Hans, zh-Hant. Missing translations fall back to the built-in English message; localization never fails.
Don't want go-i18n? Implement the one-method jsonschema.Translator interface with any backend — a database, gettext, or a hardcoded map.
ErrRegexValidation.RegexPatternError, StructTagError, and UnmarshalError work with errors.As.*EvaluationResult; use IsValid, Errors, ToFlag, ToList, or ToLocalizedList depending on how much detail you need.task test # Run the full test suite with race detection
task lint # Run golangci-lint and tidy checks
task bench # Run benchmarks
task verify # Run deps, fmt, vet, lint, test, and govulncheck
For development conventions and agent-facing project rules, see CLAUDE.md.
Contributions are welcome. Open an issue for large API or behavior changes before sending a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.
76 followers · starred Jun 2025
20 followers · starred Dec 2025
8 followers · starred Oct 2025
18 followers · starred Mar 2025
Go library for JSON Schema Draft 2020-12 validation with enhanced outputs and multilingual error messages
Go
227
248 commits
updated Sep 14, 2026
A high-performance JSON Schema validator for Go with direct struct validation, default-aware unmarshaling, a fluent constructor API, and support for Draft 2020-12, Draft 2019-09, Draft-07, Draft-06, and Draft-04.
schema.Validate(input) accepts raw JSON, maps, or Go structs.encoding/json.Number instead of being rounded through float64.schema.Unmarshal applies schema defaults; validation stays a separate step.Object, Prop, String, Required, and composition helpers.FromStruct.i18n subpackage, or implement the one-method Translator interface yourself.go get github.com/kaptinlin/jsonschema
Requires the Go version declared in go.mod.
package main
import (
"fmt"
"log"
"github.com/kaptinlin/jsonschema"
)
type User struct {
Name string `json:"name"`
Country string `json:"country"`
}
func main() {
schema, err := jsonschema.NewCompiler().Compile([]byte(`{
"type": "object",
"properties": {
"name": {"type": "string", "minLength": 1},
"country": {"type": "string", "default": "US"}
},
"required": ["name"]
}`))
if err != nil {
log.Fatal(err)
}
data := []byte(`{"name":"Alice"}`)
result := schema.Validate(data)
if !result.IsValid() {
log.Fatalf("invalid payload: %v", result.Errors)
}
var user User
if err := schema.Unmarshal(&user, data); err != nil {
log.Fatal(err)
}
fmt.Printf("%+v\n", user)
}
schema.Validate is the main entry point. Pass raw JSON, a decoded map, or a Go struct — it dispatches internally.
result := schema.Validate([]byte(`{"name":"Alice"}`))
result = schema.Validate(map[string]any{"name": "Alice"})
result = schema.Validate(User{Name: "Alice"})
When the input type is known and you want to skip the dispatch and any extra conversion, call the type-specific method directly. They share semantics with Validate.
| Method | Use When |
|---|---|
ValidateJSON([]byte) | Hot paths handling raw JSON request bodies or stored documents |
ValidateMap(map[string]any) | Already-decoded JSON objects you do not want re-encoded |
ValidateStruct(any) | Go values, when you want to avoid a JSON round-trip |
The package's default JSON codec preserves every untyped number as
encoding/json.Number. Numeric constraints, enum, const, uniqueItems,
and numeric format callbacks compare values mathematically, including integers
beyond the exact range of float64.
import stdjson "encoding/json"
numberSchema := jsonschema.Number()
var value any
if err := numberSchema.Unmarshal(&value, []byte(`18446744073709551615`)); err != nil {
log.Fatal(err)
}
number := value.(stdjson.Number)
fmt.Println(number.String()) // 18446744073709551615
Direct Go inputs retain their native numeric types. Use jsonschema.NewRat in
numeric custom formats when the callback must accept both native Go numbers and
encoding/json.Number. A codec installed with WithDecoderJSON or
WithEncoderJSON replaces the corresponding default behavior, so the caller
owns its dynamic number types and precision guarantees.
When passing untyped values to a non-JSON serializer, adapt
encoding/json.Number at that serializer boundary. Do not convert through
float64; emit an exact target-format number or return an error.
Schemas are compiled using the dialect declared by $schema. When $schema is absent, the compiler defaults to Draft 2020-12.
compiler := jsonschema.NewCompiler()
compiler.SetDefaultDialect(jsonschema.Draft7) // for schemas without "$schema"
schema, err := compiler.Compile(schemaBytes)
Supported dialects are:
| Dialect | Constant |
|---|---|
| Draft 2020-12 | jsonschema.Draft202012 |
| Draft 2019-09 | jsonschema.Draft201909 |
| Draft-07 | jsonschema.Draft7 |
| Draft-06 | jsonschema.Draft6 |
| Draft-04 | jsonschema.Draft4 |
format remains annotation-only in the standard Draft 2020-12 dialect. Custom
dialects can require Format-Assertion, and callers can independently enable
best-effort assertion with SetAssertFormat(true).
Compile does not perform schema meta-validation by default; call
ValidateSchema when the schema document itself is untrusted.
result, err := compiler.ValidateSchema(schemaBytes)
if err != nil {
log.Fatal(err)
}
if !result.IsValid() {
log.Fatalf("invalid schema document: %v", result.Errors)
}
See docs/dialects.md for compatibility details.
Build schemas directly in Go when you want type-safe composition instead of raw JSON literals.
userSchema := jsonschema.Object(
jsonschema.Prop("name", jsonschema.String(jsonschema.MinLength(1))),
jsonschema.Prop("email", jsonschema.Email()),
jsonschema.Prop("age", jsonschema.Integer(jsonschema.Min(0))),
jsonschema.Required("name", "email"),
)
result := userSchema.Validate(map[string]any{
"name": "Alice",
"email": "alice@example.com",
"age": 30,
})
fmt.Println(result.IsValid())
The constructor layer also includes composition helpers such as OneOf, AnyOf, AllOf, Not, and If(...).Then(...).Else(...).
Generate schemas from Go types at runtime with FromStruct, or use schemagen for generated code.
type Signup struct {
Name string `jsonschema:"required,minLength=2,maxLength=50"`
Email string `jsonschema:"required,format=email"`
Age int `jsonschema:"minimum=18"`
}
schema, err := jsonschema.FromStruct[Signup]()
if err != nil {
log.Fatal(err)
}
fmt.Println(schema.Validate(map[string]any{
"name": "Alice",
"email": "alice@example.com",
"age": 28,
}).IsValid())
Install the generator when you want compile-time helpers:
go install github.com/kaptinlin/jsonschema/cmd/schemagen@latest
schemagen
Use FromStructWithOptions when you need a custom tag name, schema version, required-field ordering, or schema-level properties.
Format validation is annotation-only by default. Enable best-effort assertion explicitly and register your own validators when application policy requires it. Schemas using a custom Format-Assertion dialect assert formats automatically.
compiler := jsonschema.NewCompiler().SetAssertFormat(true)
compiler.RegisterFormat("customer-id", func(v any) bool {
s, ok := v.(string)
return ok && strings.HasPrefix(s, "CUST-")
}, "string")
Register default functions on a compiler, then unmarshal validated data through schemas that use those defaults.
compiler := jsonschema.NewCompiler()
compiler.RegisterDefaultFunc("now", jsonschema.DefaultNowFunc)
CompileBatch for interdependent resources. Compilation rejects missing
references and duplicate resource definitions; use Compiler.Schema to reuse
a published definition. Concurrent loading of a shared dependency reuses the
first published resource. Later compilation never rebinds an existing graph.MarshalJSON followed by Compile once; direct
validation of mutable builders checks the entire graph for unresolved references.SetPreserveExtra(true) when tools need to keep non-standard extension keywords in Schema.Extra.RegisterLoader. http and https ship pre-registered with a 10s timeout. If your schemas come from untrusted sources, replace or remove those loaders so external $ref resolution is gated by your own host/size/timeout policy.The root package knows nothing about translation frameworks — pure validation users pay zero Intl compile, link, and binary cost. Localization lives in the optional i18n subpackage:
import "github.com/kaptinlin/jsonschema/i18n"
zh, err := i18n.New("zh-Hans") // one translator per locale
if err != nil {
log.Fatal(err)
}
localized := result.ToLocalizedList(zh)
fmt.Println(localized.Valid)
for path, message := range result.LocalizedDetailedErrors(zh) {
fmt.Printf("%s: %s\n", path, message)
}
Built-in locales: en, de-DE, es-ES, fr-FR, ja-JP, ko-KR, pt-BR, zh-Hans, zh-Hant. Missing translations fall back to the built-in English message; localization never fails.
Don't want go-i18n? Implement the one-method jsonschema.Translator interface with any backend — a database, gettext, or a hardcoded map.
ErrRegexValidation.RegexPatternError, StructTagError, and UnmarshalError work with errors.As.*EvaluationResult; use IsValid, Errors, ToFlag, ToList, or ToLocalizedList depending on how much detail you need.task test # Run the full test suite with race detection
task lint # Run golangci-lint and tidy checks
task bench # Run benchmarks
task verify # Run deps, fmt, vet, lint, test, and govulncheck
For development conventions and agent-facing project rules, see CLAUDE.md.
Contributions are welcome. Open an issue for large API or behavior changes before sending a pull request.
This project is licensed under the MIT License - see the LICENSE file for details.
76 followers · starred Jun 2025
20 followers · starred Dec 2025
8 followers · starred Oct 2025
18 followers · starred Mar 2025