Repository: getkin/kin-openapi (go)

Issue:
openapi2conv: Operation-level externalDocs are not preserved
openapi2conv does not preserve `externalDocs` defined on an operation.

## Input
```yaml
swagger: '2.0'
info:
  version: 1.0.0
  title: title
paths:
  /test:
    get:
      externalDocs:
        url: https://example/operation
        description: Operation level documentation
      responses:
        '200':
          description: description
```

## Expected output
```yaml
openapi: 3.0.3
info:
  version: 1.0.0
  title: title
paths:
  /test:
    get:
      externalDocs:
        url: https://example/operation
        description: Operation level documentation
      responses:
        '200':
          description: description
```

## Actual output
```yaml
openapi: 3.0.3
info:
  version: 1.0.0
  title: title
paths:
  /test:
    get:
      responses:
        '200':
          description: description
```

## Additional context
Root-level and schema-level externalDocs fields are preserved correctly.

Pull request:
openapi2conv: Preserve externalDocs on operations during conversion
## Summary

Preserve `externalDocs` on Operations during conversion.

## Related issue

Fixes #1069

## Changes

- Sets `externalDocs` on operations during v2 → v3 conversion
- Sets `externalDocs` on operations during v3 → v2 conversion
- Adds tests verifying `externalDocs` are preserved at:
  - Root level
  - Schema level
  - Operation level

Patch:
diff --git a/openapi2conv/openapi2_conv.go b/openapi2conv/openapi2_conv.go
index c32d1a6..4bb051b 100644
--- a/openapi2conv/openapi2_conv.go
+++ b/openapi2conv/openapi2_conv.go
@@ -149,12 +149,13 @@ func ToV3Operation(doc2 *openapi2.T, components *openapi3.Components, pathItem *
 		return nil, nil
 	}
 	doc3 := &openapi3.Operation{
-		OperationID: operation.OperationID,
-		Summary:     operation.Summary,
-		Description: operation.Description,
-		Deprecated:  operation.Deprecated,
-		Tags:        operation.Tags,
-		Extensions:  stripNonExtensions(operation.Extensions),
+		OperationID:  operation.OperationID,
+		Summary:      operation.Summary,
+		Description:  operation.Description,
+		Deprecated:   operation.Deprecated,
+		Tags:         operation.Tags,
+		Extensions:   stripNonExtensions(operation.Extensions),
+		ExternalDocs: operation.ExternalDocs,
 	}
 	if v := operation.Security; v != nil {
 		doc3Security := ToV3SecurityRequirements(*v)
@@ -1065,12 +1066,13 @@ func FromV3Operation(doc3 *openapi3.T, operation *openapi3.Operation) (*openapi2
 		return nil, nil
 	}
 	result := &openapi2.Operation{
-		OperationID: operation.OperationID,
-		Summary:     operation.Summary,
-		Description: operation.Description,
-		Deprecated:  operation.Deprecated,
-		Tags:        operation.Tags,
-		Extensions:  stripNonExtensions(operation.Extensions),
+		OperationID:  operation.OperationID,
+		Summary:      operation.Summary,
+		Description:  operation.Description,
+		Deprecated:   operation.Deprecated,
+		Tags:         operation.Tags,
+		Extensions:   stripNonExtensions(operation.Extensions),
+		ExternalDocs: operation.ExternalDocs,
 	}
 	if v := operation.Security; v != nil {
 		resultSecurity := FromV3SecurityRequirements(*v)


Test patch:
diff --git a/openapi2conv/issue1069_test.go b/openapi2conv/issue1069_test.go
new file mode 100644
index 0000000..f7bb8bc
--- /dev/null
+++ b/openapi2conv/issue1069_test.go
@@ -0,0 +1,209 @@
+package openapi2conv
+
+import (
+	"context"
+	"testing"
+
+	"github.com/getkin/kin-openapi/openapi2"
+	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/oasdiff/yaml"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestIssue1069V2ToV3(t *testing.T) {
+	tests := []struct {
+		name     string
+		v2Spec   string
+		validate func(t *testing.T, v3 *openapi3.T)
+	}{
+		{
+			name: "root level externalDocs",
+			v2Spec: `
+swagger: '2.0'
+info:
+  version: 1.0.0
+  title: title
+externalDocs:
+  url: https://example/root
+  description: Root level documentation
+paths:
+  /test:
+    get:
+      responses:
+        '200':
+          description: description
+`,
+			validate: func(t *testing.T, v3 *openapi3.T) {
+				require.NotNil(t, v3.ExternalDocs)
+				assert.Equal(t, "https://example/root", v3.ExternalDocs.URL)
+				assert.Equal(t, "Root level documentation", v3.ExternalDocs.Description)
+			},
+		},
+		{
+			name: "operation level externalDocs",
+			v2Spec: `
+swagger: '2.0'
+info:
+  version: 1.0.0
+  title: title
+paths:
+  /test:
+    get:
+      externalDocs:
+        url: https://example/operation
+        description: Operation level documentation
+      responses:
+        '200':
+          description: description
+`,
+			validate: func(t *testing.T, v3 *openapi3.T) {
+				op := v3.Paths.Value("/test").Get
+				require.NotNil(t, op.ExternalDocs)
+				assert.Equal(t, "https://example/operation", op.ExternalDocs.URL)
+				assert.Equal(t, "Operation level documentation", op.ExternalDocs.Description)
+			},
+		},
+		{
+			name: "schema level externalDocs",
+			v2Spec: `
+swagger: '2.0'
+info:
+  version: 1.0.0
+  title: title
+definitions:
+  TestSchema:
+    type: object
+    externalDocs:
+      url: https://example/schema
+      description: Schema level documentation
+paths:
+  /test:
+    get:
+      responses:
+        '200':
+          description: description
+          schema:
+            $ref: '#/definitions/TestSchema'
+`,
+			validate: func(t *testing.T, v3 *openapi3.T) {
+				schema := v3.Components.Schemas["TestSchema"].Value
+				require.NotNil(t, schema.ExternalDocs)
+				assert.Equal(t, "https://example/schema", schema.ExternalDocs.URL)
+				assert.Equal(t, "Schema level documentation", schema.ExternalDocs.Description)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			v3, err := v2v3YAML([]byte(tt.v2Spec))
+			require.NoError(t, err)
+			err = v3.Validate(context.Background())
+			require.NoError(t, err)
+			tt.validate(t, v3)
+		})
+	}
+}
+
+func TestIssue1069V3ToV2(t *testing.T) {
+	tests := []struct {
+		name     string
+		v3Spec   string
+		validate func(t *testing.T, v2 *openapi2.T)
+	}{
+		{
+			name: "root level externalDocs",
+			v3Spec: `
+openapi: 3.0.3
+info:
+  version: 1.0.0
+  title: title
+externalDocs:
+  url: https://example/root
+  description: Root level documentation
+components: {}
+paths:
+  /test:
+    get:
+      responses:
+        '200':
+          description: description
+`,
+			validate: func(t *testing.T, v2 *openapi2.T) {
+				require.NotNil(t, v2.ExternalDocs)
+				assert.Equal(t, "https://example/root", v2.ExternalDocs.URL)
+				assert.Equal(t, "Root level documentation", v2.ExternalDocs.Description)
+			},
+		},
+		{
+			name: "operation level externalDocs",
+			v3Spec: `
+openapi: 3.0.3
+info:
+  version: 1.0.0
+  title: title
+components: {}
+paths:
+  /test:
+    get:
+      externalDocs:
+        url: https://example/operation
+        description: Operation level documentation
+      responses:
+        '200':
+          description: description
+`,
+			validate: func(t *testing.T, v2 *openapi2.T) {
+				op := v2.Paths["/test"].Get
+				require.NotNil(t, op.ExternalDocs)
+				assert.Equal(t, "https://example/operation", op.ExternalDocs.URL)
+				assert.Equal(t, "Operation level documentation", op.ExternalDocs.Description)
+			},
+		},
+		{
+			name: "schema level externalDocs",
+			v3Spec: `
+openapi: 3.0.3
+info:
+  version: 1.0.0
+  title: title
+components:
+  schemas:
+    TestSchema:
+      type: object
+      externalDocs:
+        url: https://example/schema
+        description: Schema level documentation
+paths:
+  /test:
+    get:
+      responses:
+        '200':
+          description: description
+          content:
+            application/json:
+              schema:
+                $ref: '#/components/schemas/TestSchema'
+`,
+			validate: func(t *testing.T, v2 *openapi2.T) {
+				schema := v2.Definitions["TestSchema"].Value
+				require.NotNil(t, schema.ExternalDocs)
+				assert.Equal(t, "https://example/schema", schema.ExternalDocs.URL)
+				assert.Equal(t, "Schema level documentation", schema.ExternalDocs.Description)
+			},
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			var doc3 openapi3.T
+			err := yaml.Unmarshal([]byte(tt.v3Spec), &doc3)
+			require.NoError(t, err)
+
+			v2, err := FromV3(&doc3)
+			require.NoError(t, err)
+			tt.validate(t, v2)
+		})
+	}
+}