From fbdafe65da01d239e313592a9f27f692965ad86f Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Fri, 29 May 2026 09:20:30 -0700 Subject: [PATCH 1/4] 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 a77bdd9..913fa40 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -208,9 +208,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 69f8048..1ff6bcc 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -4238,3 +4238,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 79d2a53ce95baeaae4be254eb2abde06c175449c Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 2 Jun 2026 06:10:08 -0700 Subject: [PATCH 2/4] 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 913fa40..010cdae 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -209,14 +209,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 1ff6bcc..26529e0 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -15,6 +15,7 @@ import ( "github.com/pb33f/testify/require" "github.com/pb33f/libopenapi-validator/config" + liberrors "github.com/pb33f/libopenapi-validator/errors" "github.com/pb33f/libopenapi-validator/paths" ) @@ -4240,10 +4241,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: @@ -4270,13 +4270,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 0b9c27cd1716a43e0883a2117abedff39956ac77 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Sun, 14 Jun 2026 14:08:18 -0700 Subject: [PATCH 3/4] 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 738cb56..786fd11 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 26529e0..7a519e1 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -4319,3 +4319,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) +} From ebf97b2b1a825f96d279af910a6e94feda779033 Mon Sep 17 00:00:00 2001 From: Sebastien Tardif Date: Tue, 6 Oct 2026 09:59:21 -0700 Subject: [PATCH 4/4] fix: collapse object query param decode into one type assertion A nil map read is safe, so a missing key and a non-map value are the same failure as a nil decoded object. Keep one checked type assertion and one QueryParameterCannotBeDecoded path. Document QueryParameterCannotBeDecoded. The valid JSON and form cases, plus the invalid schema and unsupported content cases, still cover the changed lines. Signed-off-by: Sebastien Tardif --- errors/parameter_errors.go | 1 + parameters/query_parameters.go | 17 ++++------------- parameters/query_parameters_test.go | 17 ++++++----------- 3 files changed, 11 insertions(+), 24 deletions(-) diff --git a/errors/parameter_errors.go b/errors/parameter_errors.go index aa870c9..d8d56a9 100644 --- a/errors/parameter_errors.go +++ b/errors/parameter_errors.go @@ -470,6 +470,7 @@ func IncorrectCookieParamArrayNumber( } } +// QueryParameterCannotBeDecoded reports that an object query parameter value could not be decoded into a map. 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) diff --git a/parameters/query_parameters.go b/parameters/query_parameters.go index 010cdae..0d2445c 100644 --- a/parameters/query_parameters.go +++ b/parameters/query_parameters.go @@ -208,19 +208,10 @@ 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 { + // A nil map read is safe. A missing key or a non-map value + // makes this assertion false. + objMap, ok := encodedObj[params[p].Name].(map[string]interface{}) + if !ok { 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 7a519e1..6cab5a2 100644 --- a/parameters/query_parameters_test.go +++ b/parameters/query_parameters_test.go @@ -4321,9 +4321,8 @@ paths: } 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. + // A content-wrapped JSON parameter with a property of the wrong type should + // fail schema validation. spec := `openapi: 3.1.0 paths: /test: @@ -4359,10 +4358,8 @@ paths: } 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. + // A form-encoded object parameter with valid comma-separated key-value pairs + // should pass validation. spec := `openapi: 3.1.0 paths: /test: @@ -4437,10 +4434,8 @@ paths: } 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. + // application/xml is not decoded as an object. This is a different content + // type from text/plain in TestQueryParamObjectMissingKey_NoPanic. spec := `openapi: 3.1.0 paths: /test: