Repository: getkin/kin-openapi
Pull request #871 (2023-11-26 15:38:11)

openapi3: reclaims Extensions unused mem post-unmarshal


Patch:
diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go
index 88835db..69f9094 100644
--- a/openapi2/openapi2.go
+++ b/openapi2/openapi2.go
@@ -100,6 +100,9 @@ func (doc *T) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "securityDefinitions")
 	delete(x.Extensions, "security")
 	delete(x.Extensions, "tags")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*doc = T(x)
 	return nil
 }
diff --git a/openapi2/operation.go b/openapi2/operation.go
index b29f67d..0bc3db6 100644
--- a/openapi2/operation.go
+++ b/openapi2/operation.go
@@ -86,6 +86,9 @@ func (operation *Operation) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "produces")
 	delete(x.Extensions, "schemes")
 	delete(x.Extensions, "security")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*operation = Operation(x)
 	return nil
 }
diff --git a/openapi2/parameter.go b/openapi2/parameter.go
index d2c71c6..36bc299 100644
--- a/openapi2/parameter.go
+++ b/openapi2/parameter.go
@@ -171,6 +171,10 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "minItems")
 	delete(x.Extensions, "default")
 
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
+
 	*parameter = Parameter(x)
 	return nil
 }
diff --git a/openapi2/path_item.go b/openapi2/path_item.go
index 95c060e..15be872 100644
--- a/openapi2/path_item.go
+++ b/openapi2/path_item.go
@@ -77,6 +77,9 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "post")
 	delete(x.Extensions, "put")
 	delete(x.Extensions, "parameters")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*pathItem = PathItem(x)
 	return nil
 }
diff --git a/openapi2/response.go b/openapi2/response.go
index bd18f88..e6b22e1 100644
--- a/openapi2/response.go
+++ b/openapi2/response.go
@@ -55,6 +55,9 @@ func (response *Response) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "schema")
 	delete(x.Extensions, "headers")
 	delete(x.Extensions, "examples")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*response = Response(x)
 	return nil
 }
diff --git a/openapi2/security_scheme.go b/openapi2/security_scheme.go
index 5a8c278..6ce9bee 100644
--- a/openapi2/security_scheme.go
+++ b/openapi2/security_scheme.go
@@ -82,6 +82,9 @@ func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "tokenUrl")
 	delete(x.Extensions, "scopes")
 	delete(x.Extensions, "tags")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*securityScheme = SecurityScheme(x)
 	return nil
 }
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index 7401eb7..ebd272a 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -827,6 +827,9 @@ func FromV3SchemaRef(schema *openapi3.SchemaRef, components *openapi3.Components
 	}
 	if schema.Value.Nullable {
 		schema.Value.Nullable = false
+		if schema.Value.Extensions == nil {
+			schema.Value.Extensions = make(map[string]interface{})
+		}
 		schema.Value.Extensions["x-nullable"] = true
 	}
 
diff --git a/openapi3/components.go b/openapi3/components.go
index 38be414..0678ca7 100644
--- a/openapi3/components.go
+++ b/openapi3/components.go
@@ -80,6 +80,9 @@ func (components *Components) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "examples")
 	delete(x.Extensions, "links")
 	delete(x.Extensions, "callbacks")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*components = Components(x)
 	return nil
 }
diff --git a/openapi3/contact.go b/openapi3/contact.go
index f8804a2..a025803 100644
--- a/openapi3/contact.go
+++ b/openapi3/contact.go
@@ -44,6 +44,9 @@ func (contact *Contact) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "name")
 	delete(x.Extensions, "url")
 	delete(x.Extensions, "email")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*contact = Contact(x)
 	return nil
 }
diff --git a/openapi3/discriminator.go b/openapi3/discriminator.go
index 8b6b813..d45e28b 100644
--- a/openapi3/discriminator.go
+++ b/openapi3/discriminator.go
@@ -37,6 +37,9 @@ func (discriminator *Discriminator) UnmarshalJSON(data []byte) error {
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "propertyName")
 	delete(x.Extensions, "mapping")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*discriminator = Discriminator(x)
 	return nil
 }
diff --git a/openapi3/encoding.go b/openapi3/encoding.go
index dc2e544..96c08ea 100644
--- a/openapi3/encoding.go
+++ b/openapi3/encoding.go
@@ -76,6 +76,9 @@ func (encoding *Encoding) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "style")
 	delete(x.Extensions, "explode")
 	delete(x.Extensions, "allowReserved")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*encoding = Encoding(x)
 	return nil
 }
diff --git a/openapi3/example.go b/openapi3/example.go
index 8f44ad4..dc9c46a 100644
--- a/openapi3/example.go
+++ b/openapi3/example.go
@@ -74,6 +74,9 @@ func (example *Example) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "description")
 	delete(x.Extensions, "value")
 	delete(x.Extensions, "externalValue")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*example = Example(x)
 	return nil
 }
diff --git a/openapi3/extension.go b/openapi3/extension.go
index c299590..37f6b01 100644
--- a/openapi3/extension.go
+++ b/openapi3/extension.go
@@ -8,11 +8,19 @@ import (
 )
 
 func validateExtensions(ctx context.Context, extensions map[string]interface{}) error { // FIXME: newtype + Validate(...)
+	allowed := getValidationOptions(ctx).extraSiblingFieldsAllowed
+
 	var unknowns []string
 	for k := range extensions {
-		if !strings.HasPrefix(k, "x-") {
-			unknowns = append(unknowns, k)
+		if strings.HasPrefix(k, "x-") {
+			continue
+		}
+		if allowed != nil {
+			if _, ok := allowed[k]; ok {
+				continue
+			}
 		}
+		unknowns = append(unknowns, k)
 	}
 
 	if len(unknowns) != 0 {
diff --git a/openapi3/external_docs.go b/openapi3/external_docs.go
index 276a36c..318892d 100644
--- a/openapi3/external_docs.go
+++ b/openapi3/external_docs.go
@@ -42,6 +42,9 @@ func (e *ExternalDocs) UnmarshalJSON(data []byte) error {
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "description")
 	delete(x.Extensions, "url")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*e = ExternalDocs(x)
 	return nil
 }
diff --git a/openapi3/info.go b/openapi3/info.go
index 72faddc..bb3003b 100644
--- a/openapi3/info.go
+++ b/openapi3/info.go
@@ -56,6 +56,9 @@ func (info *Info) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "contact")
 	delete(x.Extensions, "license")
 	delete(x.Extensions, "version")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*info = Info(x)
 	return nil
 }
diff --git a/openapi3/license.go b/openapi3/license.go
index 59fd1af..32f7587 100644
--- a/openapi3/license.go
+++ b/openapi3/license.go
@@ -38,6 +38,9 @@ func (license *License) UnmarshalJSON(data []byte) error {
 	_ = json.Unmarshal(data, &x.Extensions)
 	delete(x.Extensions, "name")
 	delete(x.Extensions, "url")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*license = License(x)
 	return nil
 }
diff --git a/openapi3/link.go b/openapi3/link.go
index cc5d875..c77e238 100644
--- a/openapi3/link.go
+++ b/openapi3/link.go
@@ -82,6 +82,9 @@ func (link *Link) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "parameters")
 	delete(x.Extensions, "server")
 	delete(x.Extensions, "requestBody")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*link = Link(x)
 	return nil
 }
diff --git a/openapi3/media_type.go b/openapi3/media_type.go
index 2c0c412..88e61d5 100644
--- a/openapi3/media_type.go
+++ b/openapi3/media_type.go
@@ -96,6 +96,9 @@ func (mediaType *MediaType) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "example")
 	delete(x.Extensions, "examples")
 	delete(x.Extensions, "encoding")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*mediaType = MediaType(x)
 	return nil
 }
diff --git a/openapi3/openapi3.go b/openapi3/openapi3.go
index 4a855da..d0d99bd 100644
--- a/openapi3/openapi3.go
+++ b/openapi3/openapi3.go
@@ -67,6 +67,9 @@ func (doc *T) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "servers")
 	delete(x.Extensions, "tags")
 	delete(x.Extensions, "externalDocs")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*doc = T(x)
 	return nil
 }
diff --git a/openapi3/operation.go b/openapi3/operation.go
index f811f9d..99c6456 100644
--- a/openapi3/operation.go
+++ b/openapi3/operation.go
@@ -119,6 +119,9 @@ func (operation *Operation) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "security")
 	delete(x.Extensions, "servers")
 	delete(x.Extensions, "externalDocs")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*operation = Operation(x)
 	return nil
 }
diff --git a/openapi3/parameter.go b/openapi3/parameter.go
index 3ce2195..c85fa09 100644
--- a/openapi3/parameter.go
+++ b/openapi3/parameter.go
@@ -239,6 +239,9 @@ func (parameter *Parameter) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "example")
 	delete(x.Extensions, "examples")
 	delete(x.Extensions, "content")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 
 	*parameter = Parameter(x)
 	return nil
diff --git a/openapi3/path_item.go b/openapi3/path_item.go
index a6b12fa..c111228 100644
--- a/openapi3/path_item.go
+++ b/openapi3/path_item.go
@@ -103,6 +103,9 @@ func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "trace")
 	delete(x.Extensions, "servers")
 	delete(x.Extensions, "parameters")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*pathItem = PathItem(x)
 	return nil
 }
diff --git a/openapi3/request_body.go b/openapi3/request_body.go
index dab4937..6ab99b2 100644
--- a/openapi3/request_body.go
+++ b/openapi3/request_body.go
@@ -122,6 +122,9 @@ func (requestBody *RequestBody) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "description")
 	delete(x.Extensions, "required")
 	delete(x.Extensions, "content")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*requestBody = RequestBody(x)
 	return nil
 }
diff --git a/openapi3/response.go b/openapi3/response.go
index b481ee1..4707c6b 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -156,6 +156,9 @@ func (response *Response) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "headers")
 	delete(x.Extensions, "content")
 	delete(x.Extensions, "links")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*response = Response(x)
 	return nil
 }
diff --git a/openapi3/schema.go b/openapi3/schema.go
index 19c4da7..c7041b5 100644
--- a/openapi3/schema.go
+++ b/openapi3/schema.go
@@ -395,6 +395,10 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "additionalProperties")
 	delete(x.Extensions, "discriminator")
 
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
+
 	*schema = Schema(x)
 
 	if schema.Format == "date" {
diff --git a/openapi3/security_scheme.go b/openapi3/security_scheme.go
index 37a9171..fe607a1 100644
--- a/openapi3/security_scheme.go
+++ b/openapi3/security_scheme.go
@@ -118,6 +118,9 @@ func (ss *SecurityScheme) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "bearerFormat")
 	delete(x.Extensions, "flows")
 	delete(x.Extensions, "openIdConnectUrl")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*ss = SecurityScheme(x)
 	return nil
 }
@@ -272,6 +275,9 @@ func (flows *OAuthFlows) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "password")
 	delete(x.Extensions, "clientCredentials")
 	delete(x.Extensions, "authorizationCode")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*flows = OAuthFlows(x)
 	return nil
 }
@@ -349,6 +355,9 @@ func (flow *OAuthFlow) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "tokenUrl")
 	delete(x.Extensions, "refreshUrl")
 	delete(x.Extensions, "scopes")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*flow = OAuthFlow(x)
 	return nil
 }
diff --git a/openapi3/server.go b/openapi3/server.go
index 9fc99f9..a10e960 100644
--- a/openapi3/server.go
+++ b/openapi3/server.go
@@ -109,6 +109,9 @@ func (server *Server) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "url")
 	delete(x.Extensions, "description")
 	delete(x.Extensions, "variables")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*server = Server(x)
 	return nil
 }
@@ -258,6 +261,9 @@ func (serverVariable *ServerVariable) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "enum")
 	delete(x.Extensions, "default")
 	delete(x.Extensions, "description")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*serverVariable = ServerVariable(x)
 	return nil
 }
diff --git a/openapi3/tag.go b/openapi3/tag.go
index 93009a1..a2cd509 100644
--- a/openapi3/tag.go
+++ b/openapi3/tag.go
@@ -69,6 +69,9 @@ func (t *Tag) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "name")
 	delete(x.Extensions, "description")
 	delete(x.Extensions, "externalDocs")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*t = Tag(x)
 	return nil
 }
diff --git a/openapi3/validation_options.go b/openapi3/validation_options.go
index 24394ba..8982594 100644
--- a/openapi3/validation_options.go
+++ b/openapi3/validation_options.go
@@ -20,10 +20,10 @@ type validationOptionsKey struct{}
 // AllowExtraSiblingFields called as AllowExtraSiblingFields("description") makes Validate not return an error when said field appears next to a $ref.
 func AllowExtraSiblingFields(fields ...string) ValidationOption {
 	return func(options *ValidationOptions) {
+		if options.extraSiblingFieldsAllowed == nil && len(fields) != 0 {
+			options.extraSiblingFieldsAllowed = make(map[string]struct{}, len(fields))
+		}
 		for _, field := range fields {
-			if options.extraSiblingFieldsAllowed == nil {
-				options.extraSiblingFieldsAllowed = make(map[string]struct{}, len(fields))
-			}
 			options.extraSiblingFieldsAllowed[field] = struct{}{}
 		}
 	}
diff --git a/openapi3/xml.go b/openapi3/xml.go
index 34ed3be..444117d 100644
--- a/openapi3/xml.go
+++ b/openapi3/xml.go
@@ -54,6 +54,9 @@ func (xml *XML) UnmarshalJSON(data []byte) error {
 	delete(x.Extensions, "prefix")
 	delete(x.Extensions, "attribute")
 	delete(x.Extensions, "wrapped")
+	if len(x.Extensions) == 0 {
+		x.Extensions = nil
+	}
 	*xml = XML(x)
 	return nil
 }


Test patch:
diff --git a/openapi3/issue513_test.go b/openapi3/issue513_test.go
index 332b922..442eac8 100644
--- a/openapi3/issue513_test.go
+++ b/openapi3/issue513_test.go
@@ -7,6 +7,71 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+func TestExtraSiblingsInRemoteRef(t *testing.T) {
+	spec := []byte(`
+openapi: 3.0.1
+servers:
+- url: http://localhost:5000
+info:
+  version: v1
+  title: Products api
+  contact:
+    name: me
+    email: me@github.com
+  description: This is a sample
+paths:
+  /categories:
+    get:
+      summary: Provides the available categories for the store
+      operationId: list-categories
+      responses:
+        '200':
+          description: this is a desc
+          content:
+            application/json:
+              schema:
+                $ref: http://schemas.sentex.io/store/categories.json
+`[1:])
+
+	// When that site fails to respond:
+	// see https://github.com/getkin/kin-openapi/issues/495
+
+	// 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"
+	//       }
+	//     ]
+	//   }
+	// }
+
+	// 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
+	// }
+
+	sl := NewLoader()
+	sl.IsExternalRefsAllowed = true
+
+	doc, err := sl.LoadFromData(spec)
+	require.NoError(t, err)
+
+	err = doc.Validate(sl.Context, AllowExtraSiblingFields("$id", "$schema"))
+	require.NoError(t, err)
+}
+
 func TestIssue513OKWithExtension(t *testing.T) {
 	spec := `
 openapi: "3.0.3"
@@ -92,7 +157,7 @@ components:
 	require.ErrorContains(t, err, `extra sibling fields: [schema]`)
 }
 
-func TestIssue513KOMixesRefAlongWithOtherFields(t *testing.T) {
+func TestIssue513KOMixesRefAlongWithOtherFieldsDisallowed(t *testing.T) {
 	spec := `
 openapi: "3.0.3"
 info: