Skip to content

Commit

Permalink
feat: add convenience model validator
Browse files Browse the repository at this point in the history
  • Loading branch information
danielgtaylor committed Sep 8, 2023
1 parent 6bcb1e5 commit 965c478
Show file tree
Hide file tree
Showing 3 changed files with 117 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
66 changes: 66 additions & 0 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
29 changes: 29 additions & 0 deletions validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package huma

import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
Expand Down Expand Up @@ -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

Expand Down

0 comments on commit 965c478

Please sign in to comment.