Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 16 additions & 15 deletions docs/docs/features/request-validation.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,10 +111,11 @@ Nullable types will generate a type array like `"type": ["string", "null"]` whic
The following additional tags are supported on model fields:

| Tag | Description | Example |
|----------------------|--------------------------------------------|---------------------------------|
| -------------------- | ------------------------------------------ | ------------------------------- |
| `doc` | Describe the field | `doc:"Who to greet"` |
| `format` | Format hint for the field | `format:"date-time"` |
| `enum` | A comma-separated list of possible values | `enum:"one,two,three"` |
| `const` | A constant value that the field must equal | `const:"fixed-value"` |
| `default` | Default value | `default:"123"` |
| `minimum` | Minimum (inclusive) | `minimum:"1"` |
| `exclusiveMinimum` | Minimum (exclusive) | `exclusiveMinimum:"0"` |
Expand All @@ -140,7 +141,7 @@ The following additional tags are supported on model fields:
Built-in string formats include:

| Format | Description | Example |
|-----------------------------------|---------------------------------|----------------------------------------|
| --------------------------------- | ------------------------------- | -------------------------------------- |
| `date-time` | Date and time in RFC3339 format | `2021-12-31T23:59:59Z` |
| `date-time-http` | Date and time in HTTP format | `Fri, 31 Dec 2021 23:59:59 GMT` |
| `date` | Date in RFC3339 format | `2021-12-31` |
Expand Down Expand Up @@ -239,21 +240,21 @@ When enabled, any query parameter sent by the client that is not defined in the

When using custom JSON Schemas, i.e. not generated from Go structs, it's possible to utilize a few more validation rules. The following schema fields are respected by the built-in validator:

- `not` for negation
- `oneOf` for exclusive inputs
- `anyOf` for matching one-or-more
- `allOf` for schema unions
- `not` for negation
- `oneOf` for exclusive inputs
- `anyOf` for matching one-or-more
- `allOf` for schema unions

See [`huma.Schema`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Schema) for more information. Note that it may be easier to use a custom [resolver](./request-resolvers.md) to implement some of these rules.

## Dive Deeper

- Tutorial
- [Your First API](../tutorial/your-first-api.md) includes string length validation
- Reference
- [`huma.Register`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Register) registers new operations
- [`huma.Operation`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Operation) the operation
- External Links
- [JSON Schema Validation](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00)
- [OpenAPI 3.1 Schema Object](https://spec.openapis.org/oas/v3.1.0#schema-object)
- [OpenAPI 3.1 Operation Object](https://spec.openapis.org/oas/v3.1.0#operation-object)
- Tutorial
- [Your First API](../tutorial/your-first-api.md) includes string length validation
- Reference
- [`huma.Register`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Register) registers new operations
- [`huma.Operation`](https://pkg.go.dev/github.com/danielgtaylor/huma/v2#Operation) the operation
- External Links
- [JSON Schema Validation](https://datatracker.ietf.org/doc/html/draft-bhutton-json-schema-validation-00)
- [OpenAPI 3.1 Schema Object](https://spec.openapis.org/oas/v3.1.0#schema-object)
- [OpenAPI 3.1 Operation Object](https://spec.openapis.org/oas/v3.1.0#operation-object)
11 changes: 11 additions & 0 deletions schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ type Schema struct {
AdditionalProperties any `yaml:"additionalProperties,omitempty"`
Properties map[string]*Schema `yaml:"properties,omitempty"`
Enum []any `yaml:"enum,omitempty"`
Const any `yaml:"const,omitempty"`
Minimum *float64 `yaml:"minimum,omitempty"`
ExclusiveMinimum *float64 `yaml:"exclusiveMinimum,omitempty"`
Maximum *float64 `yaml:"maximum,omitempty"`
Expand Down Expand Up @@ -156,6 +157,7 @@ type Schema struct {
// Precomputed validation messages. These prevent allocations during
// validation and are known at schema creation time.
msgEnum string `yaml:"-"`
msgConst string `yaml:"-"`
msgMinimum string `yaml:"-"`
msgExclusiveMinimum string `yaml:"-"`
msgMaximum string `yaml:"-"`
Expand Down Expand Up @@ -213,6 +215,7 @@ func (s *Schema) MarshalJSON() ([]byte, error) {
{"additionalProperties", s.AdditionalProperties, omitNil},
{"properties", props, omitEmpty},
{"enum", s.Enum, omitEmpty},
{"const", s.Const, omitNil},
{"minimum", s.Minimum, omitEmpty},
{"exclusiveMinimum", s.ExclusiveMinimum, omitEmpty},
{"maximum", s.Maximum, omitEmpty},
Expand Down Expand Up @@ -246,6 +249,9 @@ func (s *Schema) PrecomputeMessages() {
s.msgEnum = ErrorFormatter(validation.MsgExpectedOneOf, strings.Join(mapTo(s.Enum, func(v any) string {
return fmt.Sprintf("%v", v)
}), ", "))
if s.Const != nil {
s.msgConst = ErrorFormatter(validation.MsgExpectedConst, fmt.Sprintf("%v", s.Const))
}
if s.Minimum != nil {
s.msgMinimum = ErrorFormatter(validation.MsgExpectedMinimumNumber, *s.Minimum)
}
Expand Down Expand Up @@ -608,6 +614,11 @@ func SchemaFromField(registry Registry, f reflect.StructField, hint string) *Sch
s.Enum = enumValues
}

if constValue := f.Tag.Get("const"); constValue != "" {
s := targetSchema(fs)
s.Const = jsonTagValue(registry, f.Name, s, constValue)
}

fs.Nullable = boolTag(f, "nullable", fs.Nullable)
if fs.Nullable && fs.Ref != "" && registry.SchemaFromRef(fs.Ref).Type == "object" {
// Nullability is only supported for scalar types for now. Objects are
Expand Down
28 changes: 28 additions & 0 deletions schema_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,34 @@ func TestSchema(t *testing.T) {
"additionalProperties": false
}`,
},
{
name: "field-const-string",
input: struct {
StrValue string `json:"strValue" const:"fixed"`
IntValue int `json:"intValue" const:"5"`
BoolValue bool `json:"boolValue" const:"true"`
}{},
expected: `{
"type": "object",
"properties": {
"strValue": {
"type": "string",
"const": "fixed"
},
"intValue": {
"type": "integer",
"format": "int64",
"const": 5
},
"boolValue": {
"type": "boolean",
"const": true
}
},
"required": ["strValue", "intValue", "boolValue"],
"additionalProperties": false
}`,
},
{
name: "field-readonly",
input: struct {
Expand Down
7 changes: 7 additions & 0 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -586,6 +586,13 @@ func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any,
res.Add(path, v, s.msgEnum)
}
}

if s.Const != nil {
// Deep equality check for const validation
if !reflect.DeepEqual(s.Const, v) {
res.Add(path, v, s.msgConst)
}
}
}

func handleArray[T any](r Registry, s *Schema, path *PathBuffer, mode ValidateMode, res *ValidateResult, arr []T) {
Expand Down
29 changes: 29 additions & 0 deletions validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1085,6 +1085,35 @@ var validateTests = []struct {
input: map[string]any{"value": "three"},
errs: []string{"expected value to be one of \"one, two\""},
},
{
name: "const string success",
typ: reflect.TypeFor[struct {
Value string "json:\"value\" const:\"fixed\""
}](),
input: map[string]any{"value": "fixed"},
},
{
name: "const int success",
typ: reflect.TypeFor[struct {
Value int "json:\"value\" const:\"42\""
}](),
input: map[string]any{"value": 42.0},
},
{
name: "const bool success",
typ: reflect.TypeFor[struct {
Value bool "json:\"value\" const:\"true\""
}](),
input: map[string]any{"value": true},
},
{
name: "expected const",
typ: reflect.TypeFor[struct {
Value string "json:\"value\" const:\"fixed\""
}](),
input: map[string]any{"value": "not fixed"},
errs: []string{"expected value to be fixed"},
},
{
name: "optional success",
typ: reflect.TypeFor[struct {
Expand Down
1 change: 1 addition & 0 deletions validation/messages.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ var (
MsgExpectedObject = "expected object"
MsgExpectedArrayItemsUnique = "expected array items to be unique"
MsgExpectedOneOf = "expected value to be one of \"%s\""
MsgExpectedConst = "expected value to be %v"
MsgExpectedMinimumNumber = "expected number >= %v"
MsgExpectedExclusiveMinimumNumber = "expected number > %v"
MsgExpectedMaximumNumber = "expected number <= %v"
Expand Down