diff --git a/huma.go b/huma.go index d0566e6b..09aea2b2 100644 --- a/huma.go +++ b/huma.go @@ -692,8 +692,19 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) } } + if op.RequestBody != nil { + for _, mediatype := range op.RequestBody.Content { + if mediatype.Schema != nil { + // Ensure all schema validation errors are set up properly as some + // parts of the schema may have been user-supplied. + mediatype.Schema.PrecomputeMessages() + } + } + } + var inSchema *Schema if op.RequestBody != nil && op.RequestBody.Content != nil && op.RequestBody.Content["application/json"] != nil && op.RequestBody.Content["application/json"].Schema != nil { + hasInputBody = true inSchema = op.RequestBody.Content["application/json"].Schema } @@ -1263,7 +1274,7 @@ func Register[I, O any](api API, op Operation, handler func(context.Context, *I) } } - if hasInputBody { + if hasInputBody && len(inputBodyIndex) > 0 { // We need to get the body into the correct type now that it has been // validated. Benchmarks on Go 1.20 show that using `json.Unmarshal` a // second time is faster than `mapstructure.Decode` or any of the other diff --git a/huma_test.go b/huma_test.go index d4e5ade4..3856dad8 100644 --- a/huma_test.go +++ b/huma_test.go @@ -1687,7 +1687,6 @@ Content of example2.txt. }, }, } - customSchema.PrecomputeMessages() huma.Register(api, huma.Operation{ Method: http.MethodPut, @@ -2160,6 +2159,44 @@ func TestSchemaWithExample(t *testing.T) { assert.Equal(t, 1, example) } +func TestCustomSchemaErrors(t *testing.T) { + // Ensure that custom schema errors are correctly reported without having + // to manually call `schema.PrecomputeMessages()`. + _, api := humatest.New(t, huma.DefaultConfig("Test API", "1.0.0")) + + huma.Register(api, huma.Operation{ + OperationID: "test", + Method: http.MethodPost, + Path: "/test", + RequestBody: &huma.RequestBody{ + Content: map[string]*huma.MediaType{ + "application/json": { + Schema: &huma.Schema{ + Type: huma.TypeObject, + Required: []string{"test"}, + AdditionalProperties: false, + Properties: map[string]*huma.Schema{ + "test": { + Type: huma.TypeInteger, + Minimum: Ptr(10.0), + }, + }, + }, + }, + }, + }, + }, func(ctx context.Context, input *struct { + RawBody []byte + }) (*struct{}, error) { + return nil, nil + }) + + resp := api.Post("/test", map[string]any{"test": 1}) + + assert.Equal(t, http.StatusUnprocessableEntity, resp.Result().StatusCode) + assert.Contains(t, resp.Body.String(), `expected number \u003e= 10`) +} + func TestBodyRace(t *testing.T) { // Run with the following: // go test -run=TestBodyRace -race -parallel=100 diff --git a/schema.go b/schema.go index 4c11a643..98d208b9 100644 --- a/schema.go +++ b/schema.go @@ -681,7 +681,9 @@ func schemaFromType(r Registry, t reflect.Type) *Schema { v := reflect.New(t).Interface() if sp, ok := v.(SchemaProvider); ok { // Special case: type provides its own schema. Do not try to generate. - return sp.Schema(r) + custom := sp.Schema(r) + custom.PrecomputeMessages() + return custom } // Handle special cases.