这是indexloc提供的服务,不要输入任何密码
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
38 changes: 34 additions & 4 deletions validate.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ const (
ModeWriteToServer
)

// ValidateStrictCasing controls whether or not field names are case-sensitive
// during validation. This is useful for clients that may send fields in a
// different case than expected by the server. For example, a legacy client may
// send `{"Foo": "bar"}` when the server expects `{"foo": "bar"}`. This is
// disabled by default to match Go's JSON unmarshaling behavior.
var ValidateStrictCasing = false

var rxHostname = regexp.MustCompile(`^([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])(\.([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]{0,61}[a-zA-Z0-9]))*$`)
var rxURITemplate = regexp.MustCompile("^([^{]*({[^}]*})?)*$")
var rxJSONPointer = regexp.MustCompile("^(?:/(?:[^~/]|~0|~1)*)*$")
Expand Down Expand Up @@ -609,7 +616,20 @@ func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode,
continue
}

if _, ok := m[k]; !ok {
actualKey := k
_, ok := m[k]
if !ok && !ValidateStrictCasing {
for actual := range m {
if strings.EqualFold(actual, k) {
// Case-insensitive match found, so this is not an error.
actualKey = actual
ok = true
break
}
}
}

if !ok {
if !s.requiredMap[k] {
continue
}
Expand All @@ -622,13 +642,13 @@ func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode,
continue
}

if m[k] == nil && (!s.requiredMap[k] || s.Nullable) {
if m[actualKey] == nil && (!s.requiredMap[k] || s.Nullable) {
// This is a non-required field which is null, or a nullable field set
// to null, so ignore it.
continue
}

if m[k] != nil && s.DependentRequired[k] != nil {
if m[actualKey] != nil && s.DependentRequired[k] != nil {
for _, dependent := range s.DependentRequired[k] {
if m[dependent] != nil {
continue
Expand All @@ -639,14 +659,24 @@ func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode,
}

path.Push(k)
Validate(r, v, path, mode, m[k], res)
Validate(r, v, path, mode, m[actualKey], res)
path.Pop()
}

if addl, ok := s.AdditionalProperties.(bool); ok && !addl {
addlPropLoop:
for k := range m {
// No additional properties allowed.
if _, ok := s.Properties[k]; !ok {
if !ValidateStrictCasing {
for propName := range s.Properties {
if strings.EqualFold(propName, k) {
// Case-insensitive match found, so this is not an error.
continue addlPropLoop
}
}
}

path.Push(k)
res.Add(path, m, validation.MsgUnexpectedProperty)
path.Pop()
Expand Down
63 changes: 54 additions & 9 deletions validate_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,15 @@ func mapTo[A, B any](s []A, f func(A) B) []B {
}

var validateTests = []struct {
name string
typ reflect.Type
s *huma.Schema
input any
mode huma.ValidateMode
errs []string
panic string
name string
typ reflect.Type
s *huma.Schema
input any
mode huma.ValidateMode
errs []string
panic string
before func()
cleanup func()
}{
{
name: "bool success",
Expand Down Expand Up @@ -918,6 +920,34 @@ var validateTests = []struct {
input: map[any]any{"value": "should not be set"},
errs: []string{"write only property is non-zero"},
},
{
name: "case-insensive success",
typ: reflect.TypeOf(struct {
Value string `json:"value"`
}{}),
input: map[string]any{"VaLuE": "works"},
},
{
name: "case-insensive fail",
typ: reflect.TypeOf(struct {
Value string `json:"value" maxLength:"3"`
}{}),
input: map[string]any{"VaLuE": "fails"},
errs: []string{"expected length <= 3"},
},
{
name: "case-sensive fail",
before: func() { huma.ValidateStrictCasing = true },
cleanup: func() { huma.ValidateStrictCasing = false },
typ: reflect.TypeOf(struct {
Value string `json:"value"`
}{}),
input: map[string]any{"VaLuE": "fails due to casing"},
errs: []string{
"expected required property value to be present",
"unexpected property",
},
},
{
name: "unexpected property",
typ: reflect.TypeOf(struct {
Expand Down Expand Up @@ -1368,6 +1398,13 @@ func TestValidate(t *testing.T) {

for _, test := range validateTests {
t.Run(test.name, func(t *testing.T) {
if test.before != nil {
test.before()
}
if test.cleanup != nil {
defer test.cleanup()
}

registry := huma.NewMapRegistry("#/components/schemas/", huma.DefaultSchemaNamer)

var s *huma.Schema
Expand Down Expand Up @@ -1502,10 +1539,18 @@ func BenchmarkValidate(b *testing.B) {
if s.Type == huma.TypeObject && s.Properties["value"] != nil {
switch i := input.(type) {
case map[string]any:
input = i["value"]
for k := range i {
if strings.EqualFold(k, "value") {
input = i[k]
}
}
s = s.Properties["value"]
case map[any]any:
input = i["value"]
for k := range i {
if strings.EqualFold(fmt.Sprintf("%v", k), "value") {
input = i[k]
}
}
s = s.Properties["value"]
}
}
Expand Down