Repository: getkin/kin-openapi
Pull request #225 (2020-06-12 16:32:32)

Swagger2 Extensions
Added extensions, marshalling/unmarshalling and conversion to and from V3 for:
- Root swagger
- Path 
- Operation
- Parameter (& v3 Requestbody)
- Responses
- SecurityDefinitions


FrancisLennon17: @fenollp Could you take another look. jsoninfo package made that a lot easier 🤦 
FrancisLennon17: @fenollp Made the changes you requested there. 

Patch:
diff --git a/openapi2/openapi2.go b/openapi2/openapi2.go
index 3da171d..f6b113f 100644
--- a/openapi2/openapi2.go
+++ b/openapi2/openapi2.go
@@ -11,10 +11,12 @@ import (
 	"fmt"
 	"net/http"
 
+	"github.com/getkin/kin-openapi/jsoninfo"
 	"github.com/getkin/kin-openapi/openapi3"
 )
 
 type Swagger struct {
+	openapi3.ExtensionProps
 	Info                openapi3.Info                  `json:"info"`
 	ExternalDocs        *openapi3.ExternalDocs         `json:"externalDocs,omitempty"`
 	Schemes             []string                       `json:"schemes,omitempty"`
@@ -29,6 +31,14 @@ type Swagger struct {
 	Tags                openapi3.Tags                  `json:"tags,omitempty"`
 }
 
+func (swagger *Swagger) MarshalJSON() ([]byte, error) {
+	return jsoninfo.MarshalStrictStruct(swagger)
+}
+
+func (swagger *Swagger) UnmarshalJSON(data []byte) error {
+	return jsoninfo.UnmarshalStrictStruct(data, swagger)
+}
+
 func (swagger *Swagger) AddOperation(path string, method string, operation *Operation) {
 	paths := swagger.Paths
 	if paths == nil {
@@ -44,6 +54,7 @@ func (swagger *Swagger) AddOperation(path string, method string, operation *Oper
 }
 
 type PathItem struct {
+	openapi3.ExtensionProps
 	Ref        string     `json:"$ref,omitempty"`
 	Delete     *Operation `json:"delete,omitempty"`
 	Get        *Operation `json:"get,omitempty"`
@@ -55,6 +66,14 @@ type PathItem struct {
 	Parameters Parameters `json:"parameters,omitempty"`
 }
 
+func (pathItem *PathItem) MarshalJSON() ([]byte, error) {
+	return jsoninfo.MarshalStrictStruct(pathItem)
+}
+
+func (pathItem *PathItem) UnmarshalJSON(data []byte) error {
+	return jsoninfo.UnmarshalStrictStruct(data, pathItem)
+}
+
 func (pathItem *PathItem) Operations() map[string]*Operation {
 	operations := make(map[string]*Operation, 8)
 	if v := pathItem.Delete; v != nil {
@@ -124,6 +143,7 @@ func (pathItem *PathItem) SetOperation(method string, operation *Operation) {
 }
 
 type Operation struct {
+	openapi3.ExtensionProps
 	Summary      string                 `json:"summary,omitempty"`
 	Description  string                 `json:"description,omitempty"`
 	ExternalDocs *openapi3.ExternalDocs `json:"externalDocs,omitempty"`
@@ -136,9 +156,18 @@ type Operation struct {
 	Security     *SecurityRequirements  `json:"security,omitempty"`
 }
 
+func (operation *Operation) MarshalJSON() ([]byte, error) {
+	return jsoninfo.MarshalStrictStruct(operation)
+}
+
+func (operation *Operation) UnmarshalJSON(data []byte) error {
+	return jsoninfo.UnmarshalStrictStruct(data, operation)
+}
+
 type Parameters []*Parameter
 
 type Parameter struct {
+	openapi3.ExtensionProps
 	Ref          string              `json:"$ref,omitempty"`
 	In           string              `json:"in,omitempty"`
 	Name         string              `json:"name,omitempty"`
@@ -162,7 +191,16 @@ type Parameter struct {
 	Default      interface{}         `json:"default,omitempty"`
 }
 
+func (parameter *Parameter) MarshalJSON() ([]byte, error) {
+	return jsoninfo.MarshalStrictStruct(parameter)
+}
+
+func (parameter *Parameter) UnmarshalJSON(data []byte) error {
+	return jsoninfo.UnmarshalStrictStruct(data, parameter)
+}
+
 type Response struct {
+	openapi3.ExtensionProps
 	Ref         string                 `json:"$ref,omitempty"`
 	Description string                 `json:"description,omitempty"`
 	Schema      *openapi3.SchemaRef    `json:"schema,omitempty"`
@@ -170,6 +208,14 @@ type Response struct {
 	Examples    map[string]interface{} `json:"examples,omitempty"`
 }
 
+func (response *Response) MarshalJSON() ([]byte, error) {
+	return jsoninfo.MarshalStrictStruct(response)
+}
+
+func (response *Response) UnmarshalJSON(data []byte) error {
+	return jsoninfo.UnmarshalStrictStruct(data, response)
+}
+
 type Header struct {
 	Ref         string `json:"$ref,omitempty"`
 	Description string `json:"description,omitempty"`
@@ -179,6 +225,7 @@ type Header struct {
 type SecurityRequirements []map[string][]string
 
 type SecurityScheme struct {
+	openapi3.ExtensionProps
 	Ref              string            `json:"$ref,omitempty"`
 	Description      string            `json:"description,omitempty"`
 	Type             string            `json:"type,omitempty"`
@@ -190,3 +237,11 @@ type SecurityScheme struct {
 	Scopes           map[string]string `json:"scopes,omitempty"`
 	Tags             openapi3.Tags     `json:"tags,omitempty"`
 }
+
+func (securityScheme *SecurityScheme) MarshalJSON() ([]byte, error) {
+	return jsoninfo.MarshalStrictStruct(securityScheme)
+}
+
+func (securityScheme *SecurityScheme) UnmarshalJSON(data []byte) error {
+	return jsoninfo.UnmarshalStrictStruct(data, securityScheme)
+}
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index 6e0b10e..14a77a1 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -13,11 +13,14 @@ import (
 
 // ToV3Swagger converts an OpenAPIv2 spec to an OpenAPIv3 spec
 func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
+	stripNonCustomExtensions(swagger.Extensions)
+
 	result := &openapi3.Swagger{
-		OpenAPI:    "3.0.2",
-		Info:       &swagger.Info,
-		Components: openapi3.Components{},
-		Tags:       swagger.Tags,
+		OpenAPI:        "3.0.2",
+		Info:           &swagger.Info,
+		Components:     openapi3.Components{},
+		Tags:           swagger.Tags,
+		ExtensionProps: swagger.ExtensionProps,
 	}
 	host := swagger.Host
 	if len(host) > 0 {
@@ -93,7 +96,10 @@ func ToV3Swagger(swagger *openapi2.Swagger) (*openapi3.Swagger, error) {
 }
 
 func ToV3PathItem(swagger *openapi2.Swagger, pathItem *openapi2.PathItem) (*openapi3.PathItem, error) {
-	result := &openapi3.PathItem{}
+	stripNonCustomExtensions(pathItem.Extensions)
+	result := &openapi3.PathItem{
+		ExtensionProps: pathItem.ExtensionProps,
+	}
 	for method, operation := range pathItem.Operations() {
 		resultOperation, err := ToV3Operation(swagger, pathItem, operation)
 		if err != nil {
@@ -118,11 +124,13 @@ func ToV3Operation(swagger *openapi2.Swagger, pathItem *openapi2.PathItem, opera
 	if operation == nil {
 		return nil, nil
 	}
+	stripNonCustomExtensions(operation.Extensions)
 	result := &openapi3.Operation{
-		OperationID: operation.OperationID,
-		Summary:     operation.Summary,
-		Description: operation.Description,
-		Tags:        operation.Tags,
+		OperationID:    operation.OperationID,
+		Summary:        operation.Summary,
+		Description:    operation.Description,
+		Tags:           operation.Tags,
+		ExtensionProps: operation.ExtensionProps,
 	}
 	if v := operation.Security; v != nil {
 		resultSecurity := ToV3SecurityRequirements(*v)
@@ -162,11 +170,13 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 			Ref: ToV3Ref(ref),
 		}, nil, nil
 	}
+	stripNonCustomExtensions(parameter.Extensions)
 	in := parameter.In
 	if in == "body" {
 		result := &openapi3.RequestBody{
-			Description: parameter.Description,
-			Required:    parameter.Required,
+			Description:    parameter.Description,
+			Required:       parameter.Required,
+			ExtensionProps: parameter.ExtensionProps,
 		}
 		if schemaRef := parameter.Schema; schemaRef != nil {
 			// Assume it's JSON
@@ -177,10 +187,11 @@ func ToV3Parameter(parameter *openapi2.Parameter) (*openapi3.ParameterRef, *open
 		}, nil
 	}
 	result := &openapi3.Parameter{
-		In:          in,
-		Name:        parameter.Name,
-		Description: parameter.Description,
-		Required:    parameter.Required,
+		In:             in,
+		Name:           parameter.Name,
+		Description:    parameter.Description,
+		Required:       parameter.Required,
+		ExtensionProps: parameter.ExtensionProps,
 	}
 
 	if parameter.Type != "" {
@@ -214,8 +225,10 @@ func ToV3Response(response *openapi2.Response) (*openapi3.ResponseRef, error) {
 			Ref: ToV3Ref(ref),
 		}, nil
 	}
+	stripNonCustomExtensions(response.Extensions)
 	result := &openapi3.Response{
-		Description: &response.Description,
+		Description:    &response.Description,
+		ExtensionProps: response.ExtensionProps,
 	}
 	if schemaRef := response.Schema; schemaRef != nil {
 		result.WithJSONSchemaRef(ToV3SchemaRef(schemaRef))
@@ -293,8 +306,10 @@ func ToV3SecurityScheme(securityScheme *openapi2.SecurityScheme) (*openapi3.Secu
 	if securityScheme == nil {
 		return nil, nil
 	}
+	stripNonCustomExtensions(securityScheme.Extensions)
 	result := &openapi3.SecurityScheme{
-		Description: securityScheme.Description,
+		Description:    securityScheme.Description,
+		ExtensionProps: securityScheme.ExtensionProps,
 	}
 	switch securityScheme.Type {
 	case "basic":
@@ -339,12 +354,16 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 	if err != nil {
 		return nil, err
 	}
+	stripNonCustomExtensions(swagger.Extensions)
+
 	result := &openapi2.Swagger{
-		Info:        *swagger.Info,
-		Definitions: FromV3Schemas(swagger.Components.Schemas),
-		Responses:   resultResponses,
-		Tags:        swagger.Tags,
+		Info:           *swagger.Info,
+		Definitions:    FromV3Schemas(swagger.Components.Schemas),
+		Responses:      resultResponses,
+		Tags:           swagger.Tags,
+		ExtensionProps: swagger.ExtensionProps,
 	}
+
 	isHTTPS := false
 	isHTTP := false
 	servers := swagger.Servers
@@ -375,6 +394,8 @@ func FromV3Swagger(swagger *openapi3.Swagger) (*openapi2.Swagger, error) {
 			continue
 		}
 		result.AddOperation(path, "GET", nil)
+		stripNonCustomExtensions(pathItem.Extensions)
+		addPathExtensions(result, path, pathItem.ExtensionProps)
 		for method, operation := range pathItem.Operations() {
 			if operation == nil {
 				continue
@@ -457,7 +478,10 @@ func FromV3SecurityRequirements(requirements openapi3.SecurityRequirements) open
 }
 
 func FromV3PathItem(swagger *openapi3.Swagger, pathItem *openapi3.PathItem) (*openapi2.PathItem, error) {
-	result := &openapi2.PathItem{}
+	stripNonCustomExtensions(pathItem.Extensions)
+	result := &openapi2.PathItem{
+		ExtensionProps: pathItem.ExtensionProps,
+	}
 	for method, operation := range pathItem.Operations() {
 		r, err := FromV3Operation(swagger, operation)
 		if err != nil {
@@ -493,11 +517,13 @@ func FromV3Operation(swagger *openapi3.Swagger, operation *openapi3.Operation) (
 	if operation == nil {
 		return nil, nil
 	}
+	stripNonCustomExtensions(operation.Extensions)
 	result := &openapi2.Operation{
-		OperationID: operation.OperationID,
-		Summary:     operation.Summary,
-		Description: operation.Description,
-		Tags:        operation.Tags,
+		OperationID:    operation.OperationID,
+		Summary:        operation.Summary,
+		Description:    operation.Description,
+		Tags:           operation.Tags,
+		ExtensionProps: operation.ExtensionProps,
 	}
 	if v := operation.Security; v != nil {
 		resultSecurity := FromV3SecurityRequirements(*v)
@@ -542,11 +568,13 @@ func FromV3RequestBody(swagger *openapi3.Swagger, operation *openapi3.Operation,
 	if name == "" {
 		return nil, errors.New("Could not find a name for request body")
 	}
+	stripNonCustomExtensions(requestBody.Extensions)
 	result := &openapi2.Parameter{
-		In:          "body",
-		Name:        name,
-		Description: requestBody.Description,
-		Required:    requestBody.Required,
+		In:             "body",
+		Name:           name,
+		Description:    requestBody.Description,
+		Required:       requestBody.Required,
+		ExtensionProps: requestBody.ExtensionProps,
 	}
 
 	// Add JSON schema
@@ -567,11 +595,13 @@ func FromV3Parameter(ref *openapi3.ParameterRef) (*openapi2.Parameter, error) {
 	if parameter == nil {
 		return nil, nil
 	}
+	stripNonCustomExtensions(parameter.Extensions)
 	result := &openapi2.Parameter{
-		Description: parameter.Description,
-		In:          parameter.In,
-		Name:        parameter.Name,
-		Required:    parameter.Required,
+		Description:    parameter.Description,
+		In:             parameter.In,
+		Name:           parameter.Name,
+		Required:       parameter.Required,
+		ExtensionProps: parameter.ExtensionProps,
 	}
 	if schemaRef := parameter.Schema; schemaRef != nil {
 		schemaRef = FromV3SchemaRef(schemaRef)
@@ -621,8 +651,10 @@ func FromV3Response(ref *openapi3.ResponseRef) (*openapi2.Response, error) {
 	if desc := response.Description; desc != nil {
 		description = *desc
 	}
+	stripNonCustomExtensions(response.Extensions)
 	result := &openapi2.Response{
-		Description: description,
+		Description:    description,
+		ExtensionProps: response.ExtensionProps,
 	}
 	if content := response.Content; content != nil {
 		if ct := content["application/json"]; ct != nil {
@@ -637,9 +669,11 @@ func FromV3SecurityScheme(swagger *openapi3.Swagger, ref *openapi3.SecuritySchem
 	if securityScheme == nil {
 		return nil, nil
 	}
+	stripNonCustomExtensions(securityScheme.Extensions)
 	result := &openapi2.SecurityScheme{
-		Ref:         FromV3Ref(ref.Ref),
-		Description: securityScheme.Description,
+		Ref:            FromV3Ref(ref.Ref),
+		Description:    securityScheme.Description,
+		ExtensionProps: securityScheme.ExtensionProps,
 	}
 	switch securityScheme.Type {
 	case "http":
@@ -684,3 +718,25 @@ var attemptedBodyParameterNames = []string{
 	"body",
 	"requestBody",
 }
+
+func stripNonCustomExtensions(extensions map[string]interface{}) {
+	for extName, _ := range extensions {
+		if !strings.HasPrefix(extName, "x-") {
+			delete(extensions, extName)
+		}
+	}
+}
+
+func addPathExtensions(swagger *openapi2.Swagger, path string, extensionProps openapi3.ExtensionProps) {
+	paths := swagger.Paths
+	if paths == nil {
+		paths = make(map[string]*openapi2.PathItem, 8)
+		swagger.Paths = paths
+	}
+	pathItem := paths[path]
+	if pathItem == nil {
+		pathItem = &openapi2.PathItem{}
+		paths[path] = pathItem
+	}
+	pathItem.ExtensionProps = extensionProps
+}


Test patch:
diff --git a/openapi2conv/openapi2_conv_test.go b/openapi2conv/openapi2_conv_test.go
index 2eeefec..d0e659b 100644
--- a/openapi2conv/openapi2_conv_test.go
+++ b/openapi2conv/openapi2_conv_test.go
@@ -35,7 +35,9 @@ func TestConvOpenAPIV2ToV3(t *testing.T) {
 
 const exampleV2 = `
 {
-  "info": {"title":"MyAPI","version":"0.1"},
+  "x-root": "root extension 1",
+  "x-root2": "root extension 2",
+  "info": {"title":"MyAPI","version":"0.1","x-info":"info extension"},
   "schemes": ["https"],
   "host": "test.example.com",
   "basePath": "/v2",
@@ -60,7 +62,10 @@ const exampleV2 = `
 		]
     },
     "/example": {
+      "x-path": "path extension 1",
+      "x-path2": "path extension 2",
       "delete": {
+        "x-operation": "operation extension 1",
         "description": "example delete",
         "responses": {
           "default": {
@@ -83,6 +88,7 @@ const exampleV2 = `
         ],
         "parameters": [
           {
+            "x-parameter": "parameter extension 1",
             "in": "query",
             "name": "x"
           },
@@ -108,6 +114,7 @@ const exampleV2 = `
           },
           {
             "in": "body",
+            "x-requestBody": "requestbody extension 1",
             "name": "body",
             "schema": {}
           }
@@ -123,6 +130,7 @@ const exampleV2 = `
             }
           },
           "default": {
+            "x-response": "response extension 1",
             "description": "default response"
           },
           "404": {
@@ -217,8 +225,10 @@ const exampleV2 = `
 
 const exampleV3 = `
 {
+  "x-root": "root extension 1",
+  "x-root2": "root extension 2",
   "openapi": "3.0.2",
-  "info": {"title":"MyAPI","version":"0.1"},
+  "info": {"title":"MyAPI","version":"0.1","x-info":"info extension"},
   "components": {
     "responses": {
       "ForbiddenError": {
@@ -297,7 +307,10 @@ const exampleV3 = `
 		]
     },
     "/example": {
+      "x-path": "path extension 1",
+      "x-path2": "path extension 2",
       "delete": {
+        "x-operation": "operation extension 1",
         "description": "example delete",
         "responses": {
           "default": {
@@ -320,6 +333,7 @@ const exampleV3 = `
         ],
         "parameters": [
           {
+            "x-parameter": "parameter extension 1",
             "in": "query",
             "name": "x"
           },
@@ -349,6 +363,7 @@ const exampleV3 = `
           }
         ],
         "requestBody": {
+          "x-requestBody": "requestbody extension 1",
           "content": {
             "application/json": {
               "schema": {}
@@ -370,6 +385,7 @@ const exampleV3 = `
             }
           },
           "default": {
+            "x-response": "response extension 1",
             "description": "default response"
           },
           "404": {