diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt index 78a4f7e6c..622ae2ef0 100644 --- a/.github/docs/openapi3.txt +++ b/.github/docs/openapi3.txt @@ -407,9 +407,6 @@ func (m Callbacks) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (callbacks *Callbacks) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets Callbacks to a copy of data. - type CommentFieldFor31Plus struct{ ValidationError } func (e *CommentFieldFor31Plus) As(target any) bool @@ -534,9 +531,6 @@ func NewContentWithSchemaRef(schema *SchemaRef, consumes []string) Content func (content Content) Get(mime string) *MediaType -func (content *Content) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets Content to a copy of data. - func (content Content) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if Content does not comply with the OpenAPI spec. @@ -569,7 +563,7 @@ type Discriminator struct { Origin *Origin `json:"-" yaml:"-"` PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"` + Mapping map[string]MappingRef `json:"mapping,omitempty" yaml:"mapping,omitempty"` } Discriminator is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#discriminator-object @@ -714,9 +708,6 @@ func (encoding *Encoding) WithHeaderRef(name string, ref *HeaderRef) *Encoding type Encodings map[string]*Encoding Encodings is a map of encoding objects keyed by field name. -func (encodings *Encodings) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets Encodings to a copy of data. - type ExactlyOneFieldError struct { // Fields is the set of fields, exactly one of which must be set // (e.g. ["content", "schema"]). @@ -816,9 +807,6 @@ func (m Examples) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (examples *Examples) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets Examples to a copy of data. - type ExamplesFieldFor31Plus struct{ ValidationError } func (e *ExamplesFieldFor31Plus) As(target any) bool @@ -1059,9 +1047,6 @@ func (m Headers) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (headers *Headers) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets Headers to a copy of data. - type IDFieldFor31Plus struct{ ValidationError } func (e *IDFieldFor31Plus) As(target any) bool @@ -1310,9 +1295,6 @@ func (m Links) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (links *Links) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets Links to a copy of data. - type Loader struct { // IsExternalRefsAllowed enables visiting other files. Enforced only when // ReadFromURIFunc is nil; a custom ReadFromURIFunc bypasses this flag and @@ -1529,7 +1511,7 @@ type OAuthFlow struct { AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes StringMap[string] `json:"scopes" yaml:"scopes"` // required + Scopes map[string]string `json:"scopes" yaml:"scopes"` // required } OAuthFlow is specified by OpenAPI/Swagger standard version 3. See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#oauth-flow-object @@ -1881,9 +1863,6 @@ func (m ParametersMap) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (parametersMap *ParametersMap) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets ParametersMap to a copy of data. - type PathItem struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"-" yaml:"-"` @@ -2129,9 +2108,6 @@ func (m RequestBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (requestBodies *RequestBodies) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets RequestBodies to a copy of data. - type RequestBody struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"-" yaml:"-"` @@ -2290,9 +2266,6 @@ func (m ResponseBodies) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (responseBodies *ResponseBodies) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets ResponseBodies to a copy of data. - type ResponseDescriptionRequired struct{ ValidationError } func (e *ResponseDescriptionRequired) As(target any) bool @@ -2925,9 +2898,6 @@ func (m Schemas) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets Schemas to a copy of data. - type SectionValidationError struct { Section string Cause error @@ -2952,9 +2922,6 @@ func NewSecurityRequirement() SecurityRequirement func (security SecurityRequirement) Authenticate(provider string, scopes ...string) SecurityRequirement -func (security *SecurityRequirement) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets SecurityRequirement to a copy of data. - func (security *SecurityRequirement) Validate(ctx context.Context, opts ...ValidationOption) error Validate returns an error if SecurityRequirement does not comply with the OpenAPI spec. @@ -3098,9 +3065,6 @@ func (m SecuritySchemes) JSONLookup(token string) (any, error) JSONLookup implements https://pkg.go.dev/github.com/go-openapi/jsonpointer#JSONPointable -func (securitySchemes *SecuritySchemes) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets SecuritySchemes to a copy of data. - type SerializationMethod struct { Style string Explode bool @@ -3199,9 +3163,6 @@ type ServerVariables map[string]*ServerVariable https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#server-variable-object ServerVariables is a map of ServerVariable objects keyed by variable name. -func (serverVariables *ServerVariables) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets ServerVariables to a copy of data. - type Servers []*Server Servers is specified by OpenAPI/Swagger standard version 3. @@ -3241,13 +3202,6 @@ func NewRegexpFormatValidator(pattern string) StringFormatValidator NewRegexpFormatValidator creates a new FormatValidator that uses a regular expression to validate the value. -type StringMap[V any] map[string]V - StringMap is a map[string]string that ignores the origin in the underlying - json representation. - -func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error) - UnmarshalJSON sets StringMap to a copy of data. - type T struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"-" yaml:"-"` diff --git a/README.md b/README.md index b0ee339a2..5cf2fbbab 100644 --- a/README.md +++ b/README.md @@ -312,6 +312,9 @@ for _, path := range doc.Paths.InMatchingOrder() { ## CHANGELOG: Sub-v1 breaking API changes +### v0.142.0 +* Removed the `openapi3.StringMap[V]` type (an internal helper for origin-aware map unmarshalling, obsolete since origin tracking moved to a separate `OriginTree` pass). `openapi3.Discriminator.Mapping` field type changed from `StringMap[MappingRef]` to `map[string]MappingRef`, and `openapi3.OAuthFlow.Scopes` from `StringMap[string]` to `map[string]string`. + ### v0.137.0 * Reinstated `openapi3.*Ptr(..)` funcs for Go 1.25 diff --git a/openapi3/callback.go b/openapi3/callback.go index 407f730a9..6350d7947 100644 --- a/openapi3/callback.go +++ b/openapi3/callback.go @@ -47,9 +47,3 @@ func (callback *Callback) Validate(ctx context.Context, opts ...ValidationOption return validateExtensions(ctx, callback.Extensions, callback.Origin) } - -// UnmarshalJSON sets Callbacks to a copy of data. -func (callbacks *Callbacks) UnmarshalJSON(data []byte) (err error) { - *callbacks, err = unmarshalStringMapP[CallbackRef](data) - return -} diff --git a/openapi3/content.go b/openapi3/content.go index 8bf7921a5..ab79147f8 100644 --- a/openapi3/content.go +++ b/openapi3/content.go @@ -115,9 +115,3 @@ func (content Content) Validate(ctx context.Context, opts ...ValidationOption) e } return nil } - -// UnmarshalJSON sets Content to a copy of data. -func (content *Content) UnmarshalJSON(data []byte) (err error) { - *content, err = unmarshalStringMapP[MediaType](data) - return -} diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go index d797701c3..64219d873 100644 --- a/openapi3/discriminator.go +++ b/openapi3/discriminator.go @@ -13,7 +13,7 @@ type Discriminator struct { Origin *Origin `json:"-" yaml:"-"` PropertyName string `json:"propertyName" yaml:"propertyName"` // required - Mapping StringMap[MappingRef] `json:"mapping,omitempty" yaml:"mapping,omitempty"` + Mapping map[string]MappingRef `json:"mapping,omitempty" yaml:"mapping,omitempty"` } // MappingRef is a ref to a Schema objects. Unlike SchemaRefs it is serialised diff --git a/openapi3/encoding.go b/openapi3/encoding.go index fd9316ae2..1094488a4 100644 --- a/openapi3/encoding.go +++ b/openapi3/encoding.go @@ -26,12 +26,6 @@ func NewEncoding() *Encoding { // Encodings is a map of encoding objects keyed by field name. type Encodings map[string]*Encoding -// UnmarshalJSON sets Encodings to a copy of data. -func (encodings *Encodings) UnmarshalJSON(data []byte) (err error) { - *encodings, err = unmarshalStringMapP[Encoding](data) - return -} - func (encoding *Encoding) WithHeader(name string, header *Header) *Encoding { return encoding.WithHeaderRef(name, &HeaderRef{ Value: header, diff --git a/openapi3/example.go b/openapi3/example.go index eeb4c6d4f..eeda1370e 100644 --- a/openapi3/example.go +++ b/openapi3/example.go @@ -82,9 +82,3 @@ func (example *Example) Validate(ctx context.Context, opts ...ValidationOption) return validateExtensions(ctx, example.Extensions, example.Origin) } - -// UnmarshalJSON sets Examples to a copy of data. -func (examples *Examples) UnmarshalJSON(data []byte) (err error) { - *examples, err = unmarshalStringMapP[ExampleRef](data) - return -} diff --git a/openapi3/header.go b/openapi3/header.go index 466c8ea21..36b74dd87 100644 --- a/openapi3/header.go +++ b/openapi3/header.go @@ -92,9 +92,3 @@ func (header *Header) Validate(ctx context.Context, opts ...ValidationOption) er } return nil } - -// UnmarshalJSON sets Headers to a copy of data. -func (headers *Headers) UnmarshalJSON(data []byte) (err error) { - *headers, err = unmarshalStringMapP[HeaderRef](data) - return -} diff --git a/openapi3/issue513_test.go b/openapi3/issue513_test.go index d26fb7667..55a399d02 100644 --- a/openapi3/issue513_test.go +++ b/openapi3/issue513_test.go @@ -1,11 +1,14 @@ -package openapi3 +package openapi3_test import ( "encoding/json" + "net/url" "strings" "testing" "github.com/stretchr/testify/require" + + "github.com/getkin/kin-openapi/openapi3" ) func TestExtraSiblingsInRemoteRef(t *testing.T) { @@ -34,45 +37,52 @@ paths: $ref: http://schemas.sentex.io/store/categories.json ` - // When that site fails to respond: - // see https://github.com/getkin/kin-openapi/issues/495 + resolver := func(loader *openapi3.Loader, url *url.URL) (data []byte, err error) { + switch url.String() { + case "http://schemas.sentex.io/store/categories.json": + data = []byte(` + { + "$id": "http://schemas.sentex.io/store/categories.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "array of category strings", + "type": "array", + "items": { + "allOf": [ + { + "$ref": "http://schemas.sentex.io/store/category.json" + } + ] + } + }`) - // http://schemas.sentex.io/store/categories.json - // { - // "$id": "http://schemas.sentex.io/store/categories.json", - // "$schema": "http://json-schema.org/draft-07/schema#", - // "description": "array of category strings", - // "type": "array", - // "items": { - // "allOf": [ - // { - // "$ref": "http://schemas.sentex.io/store/category.json" - // } - // ] - // } - // } + case "http://schemas.sentex.io/store/category.json": + data = []byte(` + { + "$id": "http://schemas.sentex.io/store/category.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "description": "category name for products", + "type": "string", + "pattern": "^[A-Za-z0-9\\-]+$", + "minimum": 1, + "maximum": 30 + }`) - // http://schemas.sentex.io/store/category.json - // { - // "$id": "http://schemas.sentex.io/store/category.json", - // "$schema": "http://json-schema.org/draft-07/schema#", - // "description": "category name for products", - // "type": "string", - // "pattern": "^[A-Za-z0-9\\-]+$", - // "minimum": 1, - // "maximum": 30 - // } + default: + panic(url) + } + return + } - for _, majmin := range []string{"'3.0'", "'3.1'"} { + for _, majmin := range []string{"'3.0'", "'3.1'", "'3.2'"} { t.Run(majmin, func(t *testing.T) { t.Parallel() - sl := NewLoader() - sl.IsExternalRefsAllowed = true + sl := openapi3.NewLoader() + sl.ReadFromURIFunc = resolver doc, err := sl.LoadFromData([]byte(strings.ReplaceAll(spec, "3.0.1", majmin))) require.NoError(t, err) - err = doc.Validate(sl.Context, AllowExtraSiblingFields("$id", "$schema")) + err = doc.Validate(sl.Context, openapi3.AllowExtraSiblingFields("$id", "$schema")) require.NoError(t, err) }) } @@ -114,7 +124,7 @@ components: for _, majmin := range []string{"3.0", "3.1"} { t.Run(majmin, func(t *testing.T) { t.Parallel() - sl := NewLoader() + sl := openapi3.NewLoader() doc, err := sl.LoadFromData([]byte(strings.ReplaceAll(spec, "3.0.3", majmin))) require.NoError(t, err) err = doc.Validate(sl.Context) @@ -165,7 +175,7 @@ components: for _, majmin := range []string{"3.0", "3.1"} { t.Run(majmin, func(t *testing.T) { t.Parallel() - sl := NewLoader() + sl := openapi3.NewLoader() doc, err := sl.LoadFromData([]byte(strings.ReplaceAll(spec, "3.0.3", majmin))) require.NoError(t, err) require.Contains(t, doc.Paths.Value("/v1/operation").Delete.Responses.Default().Value.Extensions, `x-my-extension`) @@ -212,7 +222,7 @@ components: for _, majmin := range []string{"3.0", "3.1"} { t.Run(majmin, func(t *testing.T) { t.Parallel() - sl := NewLoader() + sl := openapi3.NewLoader() doc, err := sl.LoadFromData([]byte(strings.ReplaceAll(spec, "3.0.3", majmin))) require.NoError(t, err) err = doc.Validate(sl.Context) @@ -258,10 +268,10 @@ components: for _, majmin := range []string{"3.0", "3.1"} { t.Run(majmin, func(t *testing.T) { t.Parallel() - sl := NewLoader() + sl := openapi3.NewLoader() doc, err := sl.LoadFromData([]byte(strings.ReplaceAll(spec, "3.0.3", majmin))) require.NoError(t, err) - err = doc.Validate(sl.Context, AllowExtraSiblingFields("description")) + err = doc.Validate(sl.Context, openapi3.AllowExtraSiblingFields("description")) require.NoError(t, err) }) } diff --git a/openapi3/link.go b/openapi3/link.go index 60f78a029..eafeb42da 100644 --- a/openapi3/link.go +++ b/openapi3/link.go @@ -91,9 +91,3 @@ func (link *Link) Validate(ctx context.Context, opts ...ValidationOption) error return validateExtensions(ctx, link.Extensions, link.Origin) } - -// UnmarshalJSON sets Links to a copy of data. -func (links *Links) UnmarshalJSON(data []byte) (err error) { - *links, err = unmarshalStringMapP[LinkRef](data) - return -} diff --git a/openapi3/media_type.go b/openapi3/media_type.go index 842b320d6..e07026028 100644 --- a/openapi3/media_type.go +++ b/openapi3/media_type.go @@ -110,7 +110,6 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error { if len(x.Extensions) == 0 { x.Extensions = nil } - delete(x.Encoding, originKey) *mediaType = MediaType(x) return nil } diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go index 287327fa2..9671b913c 100644 --- a/openapi3/openapi3.go +++ b/openapi3/openapi3.go @@ -177,7 +177,6 @@ func (doc *T) UnmarshalJSON(data []byte) error { if len(x.Extensions) == 0 { x.Extensions = nil } - delete(x.Webhooks, originKey) *doc = T(x) return nil } diff --git a/openapi3/origin.go b/openapi3/origin.go index c6890e8ca..e55695a5b 100644 --- a/openapi3/origin.go +++ b/openapi3/origin.go @@ -8,8 +8,6 @@ import ( "github.com/oasdiff/yaml" ) -const originKey = "__origin__" - var originPtrType = reflect.TypeFor[*Origin]() // Origin contains the origin of a collection. diff --git a/openapi3/parameter.go b/openapi3/parameter.go index b1010a61e..2b9962676 100644 --- a/openapi3/parameter.go +++ b/openapi3/parameter.go @@ -409,9 +409,3 @@ func (parameter *Parameter) Validate(ctx context.Context, opts ...ValidationOpti return validateExtensions(ctx, parameter.Extensions, parameter.Origin) } - -// UnmarshalJSON sets ParametersMap to a copy of data. -func (parametersMap *ParametersMap) UnmarshalJSON(data []byte) (err error) { - *parametersMap, err = unmarshalStringMapP[ParameterRef](data) - return -} diff --git a/openapi3/request_body.go b/openapi3/request_body.go index e8c52a1cb..55c9b972f 100644 --- a/openapi3/request_body.go +++ b/openapi3/request_body.go @@ -135,9 +135,3 @@ func (requestBody *RequestBody) Validate(ctx context.Context, opts ...Validation return validateExtensions(ctx, requestBody.Extensions, requestBody.Origin) } - -// UnmarshalJSON sets RequestBodies to a copy of data. -func (requestBodies *RequestBodies) UnmarshalJSON(data []byte) (err error) { - *requestBodies, err = unmarshalStringMapP[RequestBodyRef](data) - return -} diff --git a/openapi3/response.go b/openapi3/response.go index 87a52fe24..2cbf83375 100644 --- a/openapi3/response.go +++ b/openapi3/response.go @@ -217,9 +217,3 @@ func (response *Response) Validate(ctx context.Context, opts ...ValidationOption return validateExtensions(ctx, response.Extensions, response.Origin) } - -// UnmarshalJSON sets ResponseBodies to a copy of data. -func (responseBodies *ResponseBodies) UnmarshalJSON(data []byte) (err error) { - *responseBodies, err = unmarshalStringMapP[ResponseRef](data) - return -} diff --git a/openapi3/schema.go b/openapi3/schema.go index 2934971d2..4388a3981 100644 --- a/openapi3/schema.go +++ b/openapi3/schema.go @@ -3186,9 +3186,3 @@ func deepCopyJSONValue(v any) any { return v // string, float64, bool, nil — all immutable } } - -// UnmarshalJSON sets Schemas to a copy of data. -func (schemas *Schemas) UnmarshalJSON(data []byte) (err error) { - *schemas, err = unmarshalStringMapP[SchemaRef](data) - return -} diff --git a/openapi3/security_requirements.go b/openapi3/security_requirements.go index 36b6401b1..3f5bd9510 100644 --- a/openapi3/security_requirements.go +++ b/openapi3/security_requirements.go @@ -49,9 +49,3 @@ func (security *SecurityRequirement) Validate(ctx context.Context, opts ...Valid return nil } - -// UnmarshalJSON sets SecurityRequirement to a copy of data. -func (security *SecurityRequirement) UnmarshalJSON(data []byte) (err error) { - *security, err = unmarshalStringMap[[]string](data) - return -} diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go index efe7f20bb..0905b61aa 100644 --- a/openapi3/security_scheme.go +++ b/openapi3/security_scheme.go @@ -325,7 +325,7 @@ type OAuthFlow struct { AuthorizationURL string `json:"authorizationUrl,omitempty" yaml:"authorizationUrl,omitempty"` TokenURL string `json:"tokenUrl,omitempty" yaml:"tokenUrl,omitempty"` RefreshURL string `json:"refreshUrl,omitempty" yaml:"refreshUrl,omitempty"` - Scopes StringMap[string] `json:"scopes" yaml:"scopes"` // required + Scopes map[string]string `json:"scopes" yaml:"scopes"` // required } // MarshalJSON returns the JSON encoding of OAuthFlow. @@ -426,9 +426,3 @@ func (flow *OAuthFlow) validate(ctx context.Context, typ oAuthFlowType, opts ... return flow.Validate(ctx, opts...) } - -// UnmarshalJSON sets SecuritySchemes to a copy of data. -func (securitySchemes *SecuritySchemes) UnmarshalJSON(data []byte) (err error) { - *securitySchemes, err = unmarshalStringMapP[SecuritySchemeRef](data) - return -} diff --git a/openapi3/server.go b/openapi3/server.go index 181eace24..ca460d7c2 100644 --- a/openapi3/server.go +++ b/openapi3/server.go @@ -119,7 +119,6 @@ func (server *Server) UnmarshalJSON(data []byte) error { if len(x.Extensions) == 0 { x.Extensions = nil } - delete(x.Variables, originKey) *server = Server(x) return nil } @@ -244,12 +243,6 @@ func (server *Server) Validate(ctx context.Context, opts ...ValidationOption) er // ServerVariables is a map of ServerVariable objects keyed by variable name. type ServerVariables map[string]*ServerVariable -// UnmarshalJSON sets ServerVariables to a copy of data. -func (serverVariables *ServerVariables) UnmarshalJSON(data []byte) (err error) { - *serverVariables, err = unmarshalStringMapP[ServerVariable](data) - return -} - type ServerVariable struct { Extensions map[string]any `json:"-" yaml:"-"` Origin *Origin `json:"-" yaml:"-"` diff --git a/openapi3/stringmap.go b/openapi3/stringmap.go deleted file mode 100644 index f416899e7..000000000 --- a/openapi3/stringmap.go +++ /dev/null @@ -1,64 +0,0 @@ -package openapi3 - -import "encoding/json" - -// StringMap is a map[string]string that ignores the origin in the underlying json representation. -type StringMap[V any] map[string]V - -// UnmarshalJSON sets StringMap to a copy of data. -func (stringMap *StringMap[V]) UnmarshalJSON(data []byte) (err error) { - *stringMap, err = unmarshalStringMap[V](data) - return -} - -// unmarshalStringMapP unmarshals given json into a map[string]*V -func unmarshalStringMapP[V any](data []byte) (map[string]*V, error) { - var m map[string]any - if err := json.Unmarshal(data, &m); err != nil { - return nil, err - } - - result := make(map[string]*V, len(m)) - for _, k := range componentNames(m) { - value, err := deepCast[V](m[k]) - if err != nil { - return nil, err - } - result[k] = value - } - - return result, nil -} - -// unmarshalStringMap unmarshals given json into a map[string]V -func unmarshalStringMap[V any](data []byte) (map[string]V, error) { - var m map[string]any - if err := json.Unmarshal(data, &m); err != nil { - return nil, err - } - - result := make(map[string]V, len(m)) - for _, k := range componentNames(m) { - value, err := deepCast[V](m[k]) - if err != nil { - return nil, err - } - result[k] = *value - } - - return result, nil -} - -// deepCast casts any value to a value of type V. -func deepCast[V any](value any) (*V, error) { - data, err := json.Marshal(value) - if err != nil { - return nil, err - } - - var result V - if err = json.Unmarshal(data, &result); err != nil { - return nil, err - } - return &result, nil -} diff --git a/openapi3filter/req_resp_decoder.go b/openapi3filter/req_resp_decoder.go index 539603898..6e7efdeef 100644 --- a/openapi3filter/req_resp_decoder.go +++ b/openapi3filter/req_resp_decoder.go @@ -932,6 +932,13 @@ func makeObject(props map[string]string, schema *openapi3.SchemaRef) (map[string return result, nil } +// maxSliceMapToSliceGap bounds how many synthesized nil holes sliceMapToSlice +// will fill in for a sparse array before rejecting the input. Without this, +// an attacker-supplied index (e.g. from a deepObject query parameter) drives +// an allocation proportional to the index itself, regardless of how many +// elements were actually provided. +const maxSliceMapToSliceGap = 10000 + // example: map[0:map[key:true] 1:map[key:false]] -> [map[key:true] map[key:false]] func sliceMapToSlice(m map[string]any) ([]any, error) { var result []any @@ -942,6 +949,9 @@ func sliceMapToSlice(m map[string]any) ([]any, error) { if err != nil { return nil, fmt.Errorf("array indexes must be integers: %w", err) } + if key < 0 { + return nil, fmt.Errorf("array indexes must not be negative: %d", key) + } keys = append(keys, key) } max := -1 @@ -950,6 +960,12 @@ func sliceMapToSlice(m map[string]any) ([]any, error) { max = k } } + // max+1 is the size of the slice this loop is about to build; bound the + // gap between what was actually supplied (len(m)) and that size so a + // single huge index can't force an outsized allocation. + if gap := max + 1 - len(m); gap > maxSliceMapToSliceGap { + return nil, fmt.Errorf("array index %d is too sparse relative to the %d supplied items", max, len(m)) + } for i := 0; i <= max; i++ { val, ok := m[strconv.Itoa(i)] if !ok {