diff --git a/README.md b/README.md index 0f39245b..7ebc85f2 100644 --- a/README.md +++ b/README.md @@ -918,6 +918,28 @@ o.Extensions["x-cli-autoconfig"] = huma.AutoConfig{ See the [CLI AutoConfiguration](https://rest.sh/#/openapi?id=autoconfiguration) documentation for more info, including how to ask the user for custom parameters. +## Model Validation + +Huma includes a utility to make it a little easier to validate models outside of the normal HTTP request/response flow, for example on app startup to load example or default data and verify it is correct. This is just a thin wrapper around the built-in validation functionality, but abstracts away some of the boilerplate required for efficient operation and provides a simple API. + +```go +type MyExample struct { + Name string `json:"name" maxLength:"5"` + Age int `json:"age" minimum:"25"` +} + +var value any +json.Unmarshal([]byte(`{"name": "abcdefg", "age": 1}`), &value) + +validator := huma.ModelValidator() +errs := validator.Validate(reflect.TypeOf(MyExample{}), value) +if errs != nil { + fmt.Println("Validation error", errs) +} +``` + +> :whale: The `huma.ModelValidator` is **not** goroutine-safe! For more flexible validation, use the `huma.Validate` function directly and provide your own registry, path buffer, validation result struct, etc. + ## Low-Level API Huma v2 is written so that you can use the low-level API directly if you want to. This is useful if you want to add some new feature or abstraction that Huma doesn't support out of the box. Huma's own `huma.Register` function, automatic HTTP `PATCH` handlers, and the `sse` package are all built on top of the public low-level API. diff --git a/validate.go b/validate.go index f481befc..78f9c5c1 100644 --- a/validate.go +++ b/validate.go @@ -518,3 +518,69 @@ func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, } } } + +// ModelValidator is a utility for validating e.g. JSON loaded data against a +// Go struct model. It is not goroutine-safe and should not be used in HTTP +// handlers! Schemas are generated on-the-fly on first use and re-used on +// subsequent calls. This utility can be used to easily validate data outside +// of the normal request/response flow, for example on application startup: +// +// type MyExample struct { +// Name string `json:"name" maxLength:"5"` +// Age int `json:"age" minimum:"25"` +// } +// +// var value any +// json.Unmarshal([]byte(`{"name": "abcdefg", "age": 1}`), &value) +// +// validator := ModelValidator() +// errs := validator.Validate(reflect.TypeOf(MyExample{}), value) +// if errs != nil { +// fmt.Println("Validation error", errs) +// } +type ModelValidator struct { + registry Registry + pb *PathBuffer + result *ValidateResult +} + +// NewModelValidator creates a new model validator with all the components +// it needs to create schemas, validate them, and return any errors. +func NewModelValidator() *ModelValidator { + return &ModelValidator{ + registry: NewMapRegistry("#/components/schemas/", DefaultSchemaNamer), + pb: NewPathBuffer([]byte(""), 0), + result: &ValidateResult{}, + } +} + +// Validate the inputs. The type should be the Go struct with validation field +// tags and the value should be e.g. JSON loaded into an `any`. A list of +// errors is returned if validation failed, otherwise `nil`. +// +// type MyExample struct { +// Name string `json:"name" maxLength:"5"` +// Age int `json:"age" minimum:"25"` +// } +// +// var value any +// json.Unmarshal([]byte(`{"name": "abcdefg", "age": 1}`), &value) +// +// validator := ModelValidator() +// errs := validator.Validate(reflect.TypeOf(MyExample{}), value) +// if errs != nil { +// fmt.Println("Validation error", errs) +// } +func (v *ModelValidator) Validate(typ reflect.Type, value any) []error { + v.pb.Reset() + v.result.Reset() + + s := v.registry.Schema(typ, true, typ.Name()) + + Validate(v.registry, s, v.pb, ModeReadFromServer, value, v.result) + + if len(v.result.Errors) > 0 { + return v.result.Errors + } + return nil +} diff --git a/validate_test.go b/validate_test.go index 05170207..c05988c4 100644 --- a/validate_test.go +++ b/validate_test.go @@ -2,6 +2,7 @@ package huma import ( "encoding/json" + "fmt" "reflect" "strings" "testing" @@ -766,6 +767,34 @@ func TestValidate(t *testing.T) { } } +func ExampleModelValidator() { + // Define a type you want to validate. + type Model struct { + Name string `json:"name" maxLength:"5"` + Age int `json:"age" minimum:"25"` + } + + typ := reflect.TypeOf(Model{}) + + // Unmarshal some JSON into an `any` for validation. This input should not + // validate against the schema for the struct above. + var val any + json.Unmarshal([]byte(`{"name": "abcdefg", "age": 1}`), &val) + + // Validate the unmarshaled data against the type and print errors. + validator := NewModelValidator() + errs := validator.Validate(typ, val) + fmt.Println(errs) + + // Try again with valid data! + json.Unmarshal([]byte(`{"name": "foo", "age": 25}`), &val) + errs = validator.Validate(typ, val) + fmt.Println(errs) + + // Output: [expected length <= 5 (name: abcdefg) expected number >= 25 (age: 1)] + // [] +} + var BenchValidatePB *PathBuffer var BenchValidateRes *ValidateResult