From d2d6c8e9ed0ac298e71c343d98b5570da2c00048 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Fri, 29 May 2026 09:20:30 -0700 Subject: [PATCH 1/3] Fix panic on missing or non-map query parameter object Replace bare type assertion with comma-ok pattern to prevent panic when query parameter object key is absent or value is not a map. --- parameters/query_parameters.go | 14 +++++++++- parameters/query_parameters_test.go | 42 +++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 5b24d6d..bbf40ef 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -197,9 +197,21 @@ doneLooking: } } + if encodedObj == nil { + break skipValues + } + objVal, objExists := encodedObj[params[p].Name] + if !objExists || objVal == nil { + break skipValues + } + objMap, mapOk := objVal.(map[string]interface{}) + if !mapOk { + break skipValues + } + numErrors := len(validationErrors) validationErrors = append(validationErrors, - ValidateParameterSchema(sch, encodedObj[params[p].Name].(map[string]interface{}), + ValidateParameterSchema(sch, objMap, ef, "Query parameter", "The query parameter", diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 1bc9ca7..9c6b209 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -4140,3 +4140,45 @@ paths: assert.True(t, valid, "issue #91: item_count=10 with type: string should not fail with 'expected string, but got number'") assert.Empty(t, errors) } + +func TestQueryParamObjectMissingKey_NoPanic(t *testing.T) { + // This test verifies that the validator does not panic when a query parameter + // is declared as type: object with a content media type that is not JSON. + // Previously a bare type assertion on encodedObj[paramName] would panic when + // encodedObj was nil (non-JSON content wrapper) or the key was absent. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + text/plain: + schema: + type: object + properties: + color: + type: string + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + // Send a request with the filter parameter present; because the content type + // is text/plain (not application/json), the Object branch cannot decode the + // value into a map and previously panicked. + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?filter=color,blue", nil) + + // Must not panic. + assert.NotPanics(t, func() { + v.ValidateQueryParams(request) + }) +} From d002a64859e49d8fd3beae843c78853f90cb5a83 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 2 Jun 2026 06:10:08 -0700 Subject: [PATCH 2/3] fix: return validation error when object query param cannot be decoded The nil/type guards prevented the panic but also silently accepted requests where a content-wrapped object parameter could not be decoded into a map. Now each guard emits a QueryParameterCannotBeDecoded validation error before breaking, so the request correctly fails validation instead of passing silently. Add QueryParameterCannotBeDecoded error constructor (modeled after HeaderParameterCannotBeDecoded) and update the test to assert on the returned error. Add a positive test for valid JSON object parameters. Signed-off-by: Sebastien Tardif --- errors/parameter_errors.go | 25 +++++++++++++ parameters/query_parameters.go | 6 +++ parameters/query_parameters_test.go | 57 ++++++++++++++++++++++++----- 3 files changed, 79 insertions(+), 9 deletions(-) diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index 48ee25a..aa870c9 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -470,6 +470,31 @@ func IncorrectCookieParamArrayNumber( } } +func QueryParameterCannotBeDecoded(param *v3.Parameter, val string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { + keywordLocation := helpers.ConstructParameterJSONPointer(pathTemplate, operation, param.Name, "type") + specLine, specCol := paramSchemaTypeLineCol(param) + + return &ValidationError{ + ValidationType: helpers.ParameterValidation, + ValidationSubType: helpers.ParameterValidationQuery, + Message: fmt.Sprintf("Query parameter '%s' cannot be decoded", param.Name), + Reason: fmt.Sprintf("The query parameter '%s' is defined as an object, "+ + "however the value '%s' cannot be decoded as an object", param.Name, val), + SpecLine: specLine, + SpecCol: specCol, + ParameterName: param.Name, + Context: sch, + HowToFix: HowToFixInvalidEncoding, + SchemaValidationErrors: []*SchemaValidationFailure{{ + Reason: fmt.Sprintf("Query value '%s' cannot be decoded as object", val), + FieldName: param.Name, + InstancePath: []string{param.Name}, + KeywordLocation: keywordLocation, + ReferenceSchema: renderedSchema, + }}, + } +} + func IncorrectParamEncodingJSON(param *v3.Parameter, ef string, sch *base.Schema, pathTemplate string, operation string, renderedSchema string) *ValidationError { escapedPath := strings.ReplaceAll(pathTemplate, "~", "~0") escapedPath = strings.ReplaceAll(escapedPath, "/", "~1") diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index bbf40ef..b775ffb 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -198,14 +198,20 @@ doneLooking: } if encodedObj == nil { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) break skipValues } objVal, objExists := encodedObj[params[p].Name] if !objExists || objVal == nil { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) break skipValues } objMap, mapOk := objVal.(map[string]interface{}) if !mapOk { + validationErrors = append(validationErrors, + errors.QueryParameterCannotBeDecoded(params[p], ef, sch, pathValue, operation, renderedSchema)) break skipValues } diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index 9c6b209..f6289e2 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -15,6 +15,7 @@ import ( "github.com/stretchr/testify/require" "github.com/pb33f/libopenapi-validator/config" + liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/paths" ) @@ -4142,10 +4143,9 @@ paths: } func TestQueryParamObjectMissingKey_NoPanic(t *testing.T) { - // This test verifies that the validator does not panic when a query parameter - // is declared as type: object with a content media type that is not JSON. - // Previously a bare type assertion on encodedObj[paramName] would panic when - // encodedObj was nil (non-JSON content wrapper) or the key was absent. + // A content-wrapped object parameter with a non-JSON content type cannot + // be decoded into a map. The validator must not panic AND must report a + // validation error (the parameter is present but unsatisfiable). spec := `openapi: 3.1.0 paths: /test: @@ -4172,13 +4172,52 @@ paths: v := NewParameterValidator(&m.Model) - // Send a request with the filter parameter present; because the content type - // is text/plain (not application/json), the Object branch cannot decode the - // value into a map and previously panicked. request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?filter=color,blue", nil) - // Must not panic. + var valid bool + var valErrors []*liberrors.ValidationError assert.NotPanics(t, func() { - v.ValidateQueryParams(request) + valid, valErrors = v.ValidateQueryParams(request) }) + + assert.False(t, valid, "parameter present but not decodable as object should be invalid") + require.Len(t, valErrors, 1) + assert.Equal(t, "Query parameter 'filter' cannot be decoded", valErrors[0].Message) +} + +func TestQueryParamObjectContentJSON_ValidObject(t *testing.T) { + // A content-wrapped JSON parameter with a valid JSON object should validate + // successfully against the schema. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + application/json: + schema: + type: object + properties: + color: + type: string + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, `https://example.com/test?filter={"color":"blue"}`, nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.True(t, valid, "valid JSON object should pass validation") + assert.Empty(t, valErrors) } From 069d2e17493b74fadb3bcc0633eafc0ecdc37c0f Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sun, 14 Jun 2026 14:08:18 -0700 Subject: [PATCH 3/3] test: add coverage for QueryParameterCannotBeDecoded and object param paths Add unit test for QueryParameterCannotBeDecoded error constructor in errors/ package (21 lines, 0% -> 100% per-package coverage). Add integration tests for query parameter object validation: - JSON content-wrapped with invalid property type (schema error path) - Form-encoded valid and invalid objects (non-content-wrapped path) - XML content type (second encodedObj==nil guard path) These bring patch coverage from 19% to ~78% and restore the errors/ package from 98.0% to 98.9%. Signed-off-by: Sebastien Tardif --- errors/parameter_errors_test.go | 19 ++++ parameters/query_parameters_test.go | 160 ++++++++++++++++++++++++++++ 2 files changed, 179 insertions(+) diff --git a/errors/parameter_errors_test.go b/errors/parameter_errors_test.go index e9a5e51..e31e551 100644 --- a/errors/parameter_errors_test.go +++ b/errors/parameter_errors_test.go @@ -54,6 +54,25 @@ func createMockParameterWithSchema() *v3.Parameter { return v3.NewParameter(param) } +func TestQueryParameterCannotBeDecoded(t *testing.T) { + param := createMockParameterWithSchema() + sch := base.NewSchema(param.GoLow().Schema.Value.Schema()) + val := "not_an_object" + + err := QueryParameterCannotBeDecoded(param, val, sch, "/test-path", "get", "{}") + + require.NotNil(t, err) + require.Equal(t, helpers.ParameterValidation, err.ValidationType) + require.Equal(t, helpers.ParameterValidationQuery, err.ValidationSubType) + require.Equal(t, "testParam", err.ParameterName) + require.Contains(t, err.Message, "Query parameter 'testParam' cannot be decoded") + require.Contains(t, err.Reason, "'not_an_object' cannot be decoded as an object") + require.Equal(t, HowToFixInvalidEncoding, err.HowToFix) + require.NotNil(t, err.Context) + require.Len(t, err.SchemaValidationErrors, 1) + require.Contains(t, err.SchemaValidationErrors[0].Reason, "'not_an_object' cannot be decoded as object") +} + func TestIncorrectFormEncoding(t *testing.T) { param := createMockParameterWithSchema() qp := &helpers.QueryParam{ diff --git a/parameters/query_parameters_test.go b/parameters/query_parameters_test.go index f6289e2..6de08a3 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -4221,3 +4221,163 @@ paths: assert.True(t, valid, "valid JSON object should pass validation") assert.Empty(t, valErrors) } + +func TestQueryParamObjectContentJSON_InvalidObject(t *testing.T) { + // A content-wrapped JSON parameter with an invalid JSON object (wrong type + // for a property) should fail schema validation via ValidateParameterSchema, + // exercising the full happy path through guards 1-3 and into the validator. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: filter + in: query + required: false + content: + application/json: + schema: + type: object + properties: + count: + type: integer + operationId: testFilter +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, `https://example.com/test?filter={"count":"notAnInt"}`, nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.False(t, valid, "wrong property type should fail schema validation") + require.NotEmpty(t, valErrors) +} + +func TestQueryParamObjectFormEncoded_ValidObject(t *testing.T) { + // A form-encoded (default style, not content-wrapped) object parameter with + // valid comma-separated key-value pairs should pass validation. This + // exercises the ConstructParamMapFromFormEncodingArrayWithSchema path through + // all three guards and into ValidateParameterSchema. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: color + in: query + schema: + type: object + properties: + R: + type: integer + G: + type: integer + B: + type: integer + operationId: testColor +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?color=R,100,G,200,B,150", nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.True(t, valid, "valid form-encoded object should pass validation") + assert.Empty(t, valErrors) +} + +func TestQueryParamObjectFormEncoded_InvalidProperty(t *testing.T) { + // A form-encoded object with an invalid property type should fail + // schema validation, exercising the error path after the guards. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: color + in: query + schema: + type: object + properties: + R: + type: integer + G: + type: integer + B: + type: integer + operationId: testColor +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?color=R,notAnInt,G,200,B,150", nil) + + valid, valErrors := v.ValidateQueryParams(request) + + assert.False(t, valid, "invalid property type should fail schema validation") + require.NotEmpty(t, valErrors) +} + +func TestQueryParamObjectContentXML_NoPanic(t *testing.T) { + // A content-wrapped parameter with an unsupported content type (e.g. + // application/xml) hits the encodedObj==nil guard. This is a different + // content type from text/plain in TestQueryParamObjectMissingKey_NoPanic, + // confirming the guard fires for any non-JSON content type. + spec := `openapi: 3.1.0 +paths: + /test: + get: + parameters: + - name: data + in: query + required: false + content: + application/xml: + schema: + type: object + properties: + key: + type: string + operationId: testData +` + + doc, err := libopenapi.NewDocument([]byte(spec)) + require.NoError(t, err) + + m, errs := doc.BuildV3Model() + require.NoError(t, errs) + + v := NewParameterValidator(&m.Model) + + request, _ := http.NewRequest(http.MethodGet, "https://example.com/test?data=val", nil) + + var valid bool + var valErrors []*liberrors.ValidationError + assert.NotPanics(t, func() { + valid, valErrors = v.ValidateQueryParams(request) + }) + + assert.False(t, valid, "unsupported content type should be invalid") + require.Len(t, valErrors, 1) + assert.Equal(t, "Query parameter 'data' cannot be decoded", valErrors[0].Message) +}