-
-
Notifications
You must be signed in to change notification settings - Fork 157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
fix: set defaults during validation to prevent override zero values #628
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -109,16 +109,16 @@ func BenchmarkRawEcho(b *testing.B) { | |
|
||
// Read and validate params | ||
id := c.Param("id") | ||
huma.Validate(registry, strSchema, pb, huma.ModeReadFromServer, id, res) | ||
huma.ValidateAndSetDefaults(registry, strSchema, pb, huma.ModeReadFromServer, id, res) | ||
|
||
ct := r.Header.Get("Content-Type") | ||
huma.Validate(registry, strSchema, pb, huma.ModeReadFromServer, ct, res) | ||
huma.ValidateAndSetDefaults(registry, strSchema, pb, huma.ModeReadFromServer, ct, res) | ||
|
||
num, err := strconv.Atoi(c.QueryParam("num")) | ||
if err != nil { | ||
panic(err) | ||
} | ||
huma.Validate(registry, numSchema, pb, huma.ModeReadFromServer, num, res) | ||
huma.ValidateAndSetDefaults(registry, numSchema, pb, huma.ModeReadFromServer, num, res) | ||
Comment on lines
117
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider validating before type conversion. The current implementation converts the query parameter to integer before validation. This might interfere with default value handling, especially for zero values. Consider validating the raw string value first, then performing the conversion. - num, err := strconv.Atoi(c.QueryParam("num"))
- if err != nil {
- panic(err)
- }
- huma.ValidateAndSetDefaults(registry, numSchema, pb, huma.ModeReadFromServer, num, res)
+ numStr := c.QueryParam("num")
+ huma.ValidateAndSetDefaults(registry, strSchema, pb, huma.ModeReadFromServer, numStr, res)
+ if len(res.Errors) > 0 {
+ panic(res.Errors)
+ }
+ num, err := strconv.Atoi(numStr)
+ if err != nil {
+ panic(err)
+ }
|
||
|
||
// Read and validate body | ||
defer r.Body.Close() | ||
|
@@ -132,7 +132,7 @@ func BenchmarkRawEcho(b *testing.B) { | |
panic(err) | ||
} | ||
|
||
huma.Validate(registry, schema, pb, huma.ModeWriteToServer, tmp, res) | ||
huma.ValidateAndSetDefaults(registry, schema, pb, huma.ModeWriteToServer, tmp, res) | ||
if len(res.Errors) > 0 { | ||
panic(res.Errors) | ||
} | ||
|
Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -109,16 +109,16 @@ func BenchmarkRawGo(b *testing.B) { | |||||||||||||||||||||||||||||||||||||||||||
if pv, ok := v.(interface{ PathValue(string) string }); ok { | ||||||||||||||||||||||||||||||||||||||||||||
id = pv.PathValue("id") | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
huma.Validate(registry, strSchema, pb, huma.ModeReadFromServer, id, res) | ||||||||||||||||||||||||||||||||||||||||||||
huma.ValidateAndSetDefaults(registry, strSchema, pb, huma.ModeReadFromServer, id, res) | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
ct := r.Header.Get("Content-Type") | ||||||||||||||||||||||||||||||||||||||||||||
huma.Validate(registry, strSchema, pb, huma.ModeReadFromServer, ct, res) | ||||||||||||||||||||||||||||||||||||||||||||
huma.ValidateAndSetDefaults(registry, strSchema, pb, huma.ModeReadFromServer, ct, res) | ||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider adding Content-Type format validation While the change to - huma.ValidateAndSetDefaults(registry, strSchema, pb, huma.ModeReadFromServer, ct, res)
+ mediaTypeSchema := registry.Schema(reflect.TypeOf(""), false, "format:media-type")
+ huma.ValidateAndSetDefaults(registry, mediaTypeSchema, pb, huma.ModeReadFromServer, ct, res)
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
num, err := strconv.Atoi(r.URL.Query().Get("num")) | ||||||||||||||||||||||||||||||||||||||||||||
if err != nil { | ||||||||||||||||||||||||||||||||||||||||||||
panic(err) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
huma.Validate(registry, numSchema, pb, huma.ModeReadFromServer, num, res) | ||||||||||||||||||||||||||||||||||||||||||||
huma.ValidateAndSetDefaults(registry, numSchema, pb, huma.ModeReadFromServer, num, res) | ||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
117
to
+121
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling for query parameter conversion The current implementation panics on conversion failure. Consider handling the error gracefully and using the validation result to report the error. - num, err := strconv.Atoi(r.URL.Query().Get("num"))
- if err != nil {
- panic(err)
- }
- huma.ValidateAndSetDefaults(registry, numSchema, pb, huma.ModeReadFromServer, num, res)
+ numStr := r.URL.Query().Get("num")
+ num := 0
+ if numStr != "" {
+ var err error
+ num, err = strconv.Atoi(numStr)
+ if err != nil {
+ res.Errors = append(res.Errors, &huma.ValidationError{
+ Path: "num",
+ Code: "type",
+ Message: "invalid number format",
+ })
+ return
+ }
+ }
+ huma.ValidateAndSetDefaults(registry, numSchema, pb, huma.ModeReadFromServer, num, res) 📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
// Read and validate body | ||||||||||||||||||||||||||||||||||||||||||||
defer r.Body.Close() | ||||||||||||||||||||||||||||||||||||||||||||
|
@@ -132,7 +132,7 @@ func BenchmarkRawGo(b *testing.B) { | |||||||||||||||||||||||||||||||||||||||||||
panic(err) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||
huma.Validate(registry, schema, pb, huma.ModeWriteToServer, tmp, res) | ||||||||||||||||||||||||||||||||||||||||||||
huma.ValidateAndSetDefaults(registry, schema, pb, huma.ModeWriteToServer, tmp, res) | ||||||||||||||||||||||||||||||||||||||||||||
if len(res.Errors) > 0 { | ||||||||||||||||||||||||||||||||||||||||||||
panic(res.Errors) | ||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -1168,7 +1168,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) | |
} | ||
|
||
if !op.SkipValidateParams { | ||
Validate(oapi.Components.Schemas, p.Schema, pb, ModeWriteToServer, pv, res) | ||
ValidateAndSetDefaults(oapi.Components.Schemas, p.Schema, pb, ModeWriteToServer, pv, res) | ||
} | ||
} | ||
}) | ||
|
@@ -1276,7 +1276,19 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) | |
pb.Reset() | ||
pb.Push("body") | ||
count := len(res.Errors) | ||
Validate(oapi.Components.Schemas, inSchema, pb, ModeWriteToServer, parsed, res) | ||
ValidateAndSetDefaults(oapi.Components.Schemas, inSchema, pb, ModeWriteToServer, parsed, res) | ||
|
||
// Validate changes the original parsed input setting default values when needed | ||
// so we need to marshal the parsed input back to a byte buffer to get the | ||
// default values set by the validator. | ||
parsedBuff := new(strings.Builder) | ||
err := DefaultJSONFormat.Marshal(parsedBuff, parsed) | ||
if err != nil { | ||
WriteErr(api, ctx, http.StatusBadRequest, "could not set default value", err) | ||
return | ||
} | ||
body = []byte(parsedBuff.String()) | ||
|
||
Comment on lines
+1279
to
+1291
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider optimizing default value handling to avoid re-marshaling Re-marshaling the |
||
parseErrCount = len(res.Errors) - count | ||
if parseErrCount > 0 { | ||
errStatus = http.StatusUnprocessableEntity | ||
|
@@ -1307,7 +1319,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) | |
// Set defaults for any fields that were not in the input. | ||
defaults.Every(v, func(item reflect.Value, def any) { | ||
if item.IsZero() { | ||
item.Set(reflect.Indirect(reflect.ValueOf(def))) | ||
// item.Set(reflect.Indirect(reflect.ValueOf(def))) | ||
} | ||
}) | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -272,7 +272,7 @@ func validateOneOf(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v | |
found := false | ||
subRes := &ValidateResult{} | ||
for _, sub := range s.OneOf { | ||
Validate(r, sub, path, mode, v, subRes) | ||
ValidateAndSetDefaults(r, sub, path, mode, v, subRes) | ||
if len(subRes.Errors) == 0 { | ||
if found { | ||
res.Add(path, v, "expected value to match exactly one schema but matched multiple") | ||
|
@@ -290,7 +290,7 @@ func validateAnyOf(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v | |
matches := 0 | ||
subRes := &ValidateResult{} | ||
for _, sub := range s.AnyOf { | ||
Validate(r, sub, path, mode, v, subRes) | ||
ValidateAndSetDefaults(r, sub, path, mode, v, subRes) | ||
if len(subRes.Errors) == 0 { | ||
matches++ | ||
} | ||
|
@@ -338,11 +338,12 @@ func validateDiscriminator(r Registry, s *Schema, path *PathBuffer, mode Validat | |
return | ||
} | ||
|
||
Validate(r, r.SchemaFromRef(ref), path, mode, v, res) | ||
ValidateAndSetDefaults(r, r.SchemaFromRef(ref), path, mode, v, res) | ||
} | ||
|
||
// Validate an input value against a schema, collecting errors in the validation | ||
// result object. If successful, `res.Errors` will be empty. It is suggested | ||
// result object. And set default values on omitted fields | ||
// If validation is successful, `res.Errors` will be empty. It is suggested | ||
// to use a `sync.Pool` to reuse the PathBuffer and ValidateResult objects, | ||
// making sure to call `Reset()` on them before returning them to the pool. | ||
// | ||
|
@@ -353,11 +354,11 @@ func validateDiscriminator(r Registry, s *Schema, path *PathBuffer, mode Validat | |
// | ||
// var value any | ||
// json.Unmarshal([]byte(`{"foo": "bar"}`), &v) | ||
// huma.Validate(registry, schema, pb, huma.ModeWriteToServer, value, res) | ||
// huma.ValidateAndSetDefaults(registry, schema, pb, huma.ModeWriteToServer, value, res) | ||
// for _, err := range res.Errors { | ||
// fmt.Println(err.Error()) | ||
// } | ||
func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any, res *ValidateResult) { | ||
func ValidateAndSetDefaults(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any, res *ValidateResult) { | ||
// Get the actual schema if this is a reference. | ||
for s.Ref != "" { | ||
s = r.SchemaFromRef(s.Ref) | ||
|
@@ -377,13 +378,13 @@ func Validate(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, v any, | |
|
||
if s.AllOf != nil { | ||
for _, sub := range s.AllOf { | ||
Validate(r, sub, path, mode, v, res) | ||
ValidateAndSetDefaults(r, sub, path, mode, v, res) | ||
} | ||
} | ||
|
||
if s.Not != nil { | ||
subRes := &ValidateResult{} | ||
Validate(r, s.Not, path, mode, v, subRes) | ||
ValidateAndSetDefaults(r, s.Not, path, mode, v, subRes) | ||
if len(subRes.Errors) == 0 { | ||
res.Add(path, v, validation.MsgExpectedNotMatchSchema) | ||
} | ||
|
@@ -576,7 +577,7 @@ func handleArray[T any](r Registry, s *Schema, path *PathBuffer, mode ValidateMo | |
|
||
for i, item := range arr { | ||
path.PushIndex(i) | ||
Validate(r, s.Items, path, mode, item, res) | ||
ValidateAndSetDefaults(r, s.Items, path, mode, item, res) | ||
path.Pop() | ||
} | ||
} | ||
|
@@ -602,6 +603,11 @@ func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, | |
// the `for` loop never runs. | ||
readOnly := v.ReadOnly | ||
writeOnly := v.WriteOnly | ||
|
||
if m[k] == nil && v.Default != nil { | ||
m[k] = v.Default | ||
} | ||
Comment on lines
+607
to
+609
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Consider extracting duplicate default value logic. The default value setting logic is duplicated between +func setDefaultIfNil(m map[any]any, k any, defaultValue any) {
+ if m[k] == nil && defaultValue != nil {
+ m[k] = defaultValue
+ }
+}
func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, m map[string]any, res *ValidateResult) {
// ...
- if m[k] == nil && v.Default != nil {
- m[k] = v.Default
- }
+ setDefaultIfNil(unsafeMapCast(m), k, v.Default)
// ...
}
func handleMapAny(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, m map[any]any, res *ValidateResult) {
// ...
- if m[k] == nil && v.Default != nil {
- m[k] = v.Default
- }
+ setDefaultIfNil(m, k, v.Default)
// ...
}
+// unsafeMapCast performs an unsafe cast from map[string]any to map[any]any
+// This is safe because string keys are compatible with any keys
+func unsafeMapCast(m map[string]any) map[any]any {
+ return *(*map[any]any)(unsafe.Pointer(&m))
+} Also applies to: 699-701 |
||
|
||
for v.Ref != "" { | ||
v = r.SchemaFromRef(v.Ref) | ||
} | ||
|
@@ -659,7 +665,7 @@ func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, | |
} | ||
|
||
path.Push(k) | ||
Validate(r, v, path, mode, m[actualKey], res) | ||
ValidateAndSetDefaults(r, v, path, mode, m[k], res) | ||
path.Pop() | ||
} | ||
|
||
|
@@ -692,7 +698,7 @@ func handleMapString(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, | |
} | ||
|
||
path.Push(k) | ||
Validate(r, addl, path, mode, v, res) | ||
ValidateAndSetDefaults(r, addl, path, mode, v, res) | ||
path.Pop() | ||
} | ||
} | ||
|
@@ -719,6 +725,11 @@ func handleMapAny(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, m | |
// the `for` loop never runs. | ||
readOnly := v.ReadOnly | ||
writeOnly := v.WriteOnly | ||
|
||
if m[k] == nil && v.Default != nil { | ||
m[k] = v.Default | ||
} | ||
|
||
for v.Ref != "" { | ||
v = r.SchemaFromRef(v.Ref) | ||
} | ||
|
@@ -763,7 +774,7 @@ func handleMapAny(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, m | |
} | ||
|
||
path.Push(k) | ||
Validate(r, v, path, mode, m[k], res) | ||
ValidateAndSetDefaults(r, v, path, mode, m[k], res) | ||
path.Pop() | ||
} | ||
|
||
|
@@ -794,7 +805,7 @@ func handleMapAny(r Registry, s *Schema, path *PathBuffer, mode ValidateMode, m | |
kStr = fmt.Sprint(k) | ||
} | ||
path.Push(kStr) | ||
Validate(r, addl, path, mode, v, res) | ||
ValidateAndSetDefaults(r, addl, path, mode, v, res) | ||
path.Pop() | ||
} | ||
} | ||
|
@@ -858,7 +869,7 @@ func (v *ModelValidator) Validate(typ reflect.Type, value any) []error { | |
|
||
s := v.registry.Schema(typ, true, typ.Name()) | ||
|
||
Validate(v.registry, s, v.pb, ModeReadFromServer, value, v.result) | ||
ValidateAndSetDefaults(v.registry, s, v.pb, ModeReadFromServer, value, v.result) | ||
|
||
if len(v.result.Errors) > 0 { | ||
return v.result.Errors | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Consider validating before type conversion.
The current implementation returns an error for invalid numbers before
ValidateAndSetDefaults
can apply any default values. Consider moving the validation before the conversion to allow proper default handling for missing or invalid query parameters.📝 Committable suggestion