Repository: getkin/kin-openapi
Pull request #454 (2021-12-02 15:23:36)

Provide support for generating recursive types into OpenAPI doc #451 + my touches
Superseeds https://github.com/getkin/kin-openapi/pull/451


Patch:
diff --git a/README.md b/README.md
index 92e7fad..abefc2f 100644
--- a/README.md
+++ b/README.md
@@ -193,6 +193,11 @@ func arrayUniqueItemsChecker(items []interface{}) bool {
 
 ## Sub-v0 breaking API changes
 
+### v0.84.0
+* The prototype of `openapi3gen.NewSchemaRefForValue` changed:
+	* It no longer returns a map but that is still accessible under the field `(*Generator).SchemaRefs`.
+	* It now takes in an additional argument (basically `doc.Components.Schemas`) which gets written to so `$ref` cycles can be properly handled.
+
 ### v0.61.0
 * Renamed `openapi2.Swagger` to `openapi2.T`.
 * Renamed `openapi2conv.FromV3Swagger` to `openapi2conv.FromV3`.
diff --git a/openapi3filter/validate_request.go b/openapi3filter/validate_request.go
index 2f9a5f1..990b299 100644
--- a/openapi3filter/validate_request.go
+++ b/openapi3filter/validate_request.go
@@ -140,7 +140,7 @@ func ValidateParameter(ctx context.Context, input *RequestValidationInput, param
 	// Validate a parameter's value.
 	if value == nil {
 		if parameter.Required {
-			return &RequestError{Input: input, Parameter: parameter, Reason: ErrInvalidRequired.Error(), Err: ErrInvalidRequired}
+			return &RequestError{Input: input, Parameter: parameter, Err: ErrInvalidRequired}
 		}
 		return nil
 	}
diff --git a/openapi3filter/validation_error_encoder.go b/openapi3filter/validation_error_encoder.go
index 707b22d..2051869 100644
--- a/openapi3filter/validation_error_encoder.go
+++ b/openapi3filter/validation_error_encoder.go
@@ -75,7 +75,7 @@ func convertBasicRequestError(e *RequestError) *ValidationError {
 }
 
 func convertErrInvalidRequired(e *RequestError) *ValidationError {
-	if e.Reason == ErrInvalidRequired.Error() && e.Parameter != nil {
+	if e.Err == ErrInvalidRequired && e.Parameter != nil {
 		return &ValidationError{
 			Status: http.StatusBadRequest,
 			Title:  fmt.Sprintf("parameter %q in %s is required", e.Parameter.Name, e.Parameter.In),
diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go
index b4ae7b0..45577bc 100644
--- a/openapi3gen/openapi3gen.go
+++ b/openapi3gen/openapi3gen.go
@@ -52,14 +52,10 @@ func SchemaCustomizer(sc SchemaCustomizerFn) Option {
 	return func(x *generatorOpt) { x.schemaCustomizer = sc }
 }
 
-// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef.
-func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) {
+// NewSchemaRefForValue is a shortcut for NewGenerator(...).NewSchemaRefForValue(...)
+func NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas, opts ...Option) (*openapi3.SchemaRef, error) {
 	g := NewGenerator(opts...)
-	ref, err := g.GenerateSchemaRef(reflect.TypeOf(value))
-	for ref := range g.SchemaRefs {
-		ref.Ref = ""
-	}
-	return ref, g.SchemaRefs, err
+	return g.NewSchemaRefForValue(value, schemas)
 }
 
 type Generator struct {
@@ -71,6 +67,9 @@ type Generator struct {
 	// If count is 1, it's not ne
 	// An OpenAPI identifier has been assigned to each.
 	SchemaRefs map[*openapi3.SchemaRef]int
+
+	// componentSchemaRefs is a set of schemas that must be defined in the components to avoid cycles
+	componentSchemaRefs map[string]struct{}
 }
 
 func NewGenerator(opts ...Option) *Generator {
@@ -79,9 +78,10 @@ func NewGenerator(opts ...Option) *Generator {
 		f(gOpt)
 	}
 	return &Generator{
-		Types:      make(map[reflect.Type]*openapi3.SchemaRef),
-		SchemaRefs: make(map[*openapi3.SchemaRef]int),
-		opts:       *gOpt,
+		Types:               make(map[reflect.Type]*openapi3.SchemaRef),
+		SchemaRefs:          make(map[*openapi3.SchemaRef]int),
+		componentSchemaRefs: make(map[string]struct{}),
+		opts:                *gOpt,
 	}
 }
 
@@ -90,17 +90,41 @@ func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, erro
 	return g.generateSchemaRefFor(nil, t, "_root", "")
 }
 
+// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef, and updates a supplied map with any dependent component schemas if they lead to cycles
+func (g *Generator) NewSchemaRefForValue(value interface{}, schemas openapi3.Schemas) (*openapi3.SchemaRef, error) {
+	ref, err := g.GenerateSchemaRef(reflect.TypeOf(value))
+	if err != nil {
+		return nil, err
+	}
+	for ref := range g.SchemaRefs {
+		if _, ok := g.componentSchemaRefs[ref.Ref]; ok && schemas != nil {
+			schemas[ref.Ref] = &openapi3.SchemaRef{
+				Value: ref.Value,
+			}
+		}
+		if strings.HasPrefix(ref.Ref, "#/components/schemas/") {
+			ref.Value = nil
+		} else {
+			ref.Ref = ""
+		}
+	}
+	return ref, nil
+}
+
 func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type, name string, tag reflect.StructTag) (*openapi3.SchemaRef, error) {
 	if ref := g.Types[t]; ref != nil && g.opts.schemaCustomizer == nil {
 		g.SchemaRefs[ref]++
 		return ref, nil
 	}
 	ref, err := g.generateWithoutSaving(parents, t, name, tag)
+	if err != nil {
+		return nil, err
+	}
 	if ref != nil {
 		g.Types[t] = ref
 		g.SchemaRefs[ref]++
 	}
-	return ref, err
+	return ref, nil
 }
 
 func getStructField(t reflect.Type, fieldInfo jsoninfo.FieldInfo) reflect.StructField {
@@ -341,6 +365,7 @@ func (g *Generator) generateCycleSchemaRef(t reflect.Type, schema *openapi3.Sche
 		typeName = t.Name()
 	}
 
+	g.componentSchemaRefs[typeName] = struct{}{}
 	return openapi3.NewSchemaRef(fmt.Sprintf("#/components/schemas/%s", typeName), schema)
 }
 


Test patch:
diff --git a/openapi3/loader_recursive_ref_test.go b/openapi3/loader_recursive_ref_test.go
index bd65903..5b7c150 100644
--- a/openapi3/loader_recursive_ref_test.go
+++ b/openapi3/loader_recursive_ref_test.go
@@ -11,7 +11,39 @@ func TestLoaderSupportsRecursiveReference(t *testing.T) {
 	loader.IsExternalRefsAllowed = true
 	doc, err := loader.LoadFromFile("testdata/recursiveRef/openapi.yml")
 	require.NoError(t, err)
-	require.NotNil(t, doc)
-	require.NoError(t, doc.Validate(loader.Context))
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
 	require.Equal(t, "bar", doc.Paths["/foo"].Get.Responses.Get(200).Value.Content.Get("application/json").Schema.Value.Properties["foo2"].Value.Properties["foo"].Value.Properties["bar"].Value.Example)
 }
+
+func TestIssue447(t *testing.T) {
+	loader := NewLoader()
+	doc, err := loader.LoadFromData([]byte(`
+openapi: 3.0.1
+info:
+  title: Recursive refs example
+  version: "1.0"
+paths: {}
+components:
+  schemas:
+    Complex:
+      type: object
+      properties:
+        parent:
+          $ref: '#/components/schemas/Complex'
+`))
+	require.NoError(t, err)
+	err = doc.Validate(loader.Context)
+	require.NoError(t, err)
+	require.Equal(t, "object", doc.Components.
+		// Complex
+		Schemas["Complex"].
+		// parent
+		Value.Properties["parent"].
+		// parent
+		Value.Properties["parent"].
+		// parent
+		Value.Properties["parent"].
+		// type
+		Value.Type)
+}
diff --git a/openapi3filter/validation_error_test.go b/openapi3filter/validation_error_test.go
index d539c3f..6eadbd0 100644
--- a/openapi3filter/validation_error_test.go
+++ b/openapi3filter/validation_error_test.go
@@ -180,7 +180,7 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 			wantErrParam:   "status",
 			wantErrParamIn: "query",
-			wantErrReason:  ErrInvalidRequired.Error(),
+			wantErrBody:    `parameter "status" in query has an error: value is required but missing`,
 			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
 				Title: `parameter "status" in query is required`},
 		},
@@ -424,7 +424,7 @@ func getValidationTests(t *testing.T) []*validationTest {
 			},
 			wantErrParam:   "petId",
 			wantErrParamIn: "path",
-			wantErrReason:  ErrInvalidRequired.Error(),
+			wantErrBody:    `parameter "petId" in path has an error: value is required but missing`,
 			wantErrResponse: &ValidationError{Status: http.StatusBadRequest,
 				Title: `parameter "petId" in path is required`},
 		},
diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go
index 6d96db9..a6d1620 100644
--- a/openapi3gen/openapi3gen_test.go
+++ b/openapi3gen/openapi3gen_test.go
@@ -1,29 +1,177 @@
-package openapi3gen
+package openapi3gen_test
 
 import (
 	"encoding/json"
 	"errors"
+	"fmt"
 	"reflect"
 	"strconv"
 	"strings"
 	"testing"
+	"time"
 
 	"github.com/getkin/kin-openapi/openapi3"
+	"github.com/getkin/kin-openapi/openapi3gen"
 	"github.com/stretchr/testify/require"
 )
 
-type CyclicType0 struct {
-	CyclicField *CyclicType1 `json:"a"`
-}
-type CyclicType1 struct {
-	CyclicField *CyclicType0 `json:"b"`
+func ExampleGenerator_SchemaRefs() {
+	type SomeOtherType string
+	type SomeStruct struct {
+		Bool    bool                      `json:"bool"`
+		Int     int                       `json:"int"`
+		Int64   int64                     `json:"int64"`
+		Float64 float64                   `json:"float64"`
+		String  string                    `json:"string"`
+		Bytes   []byte                    `json:"bytes"`
+		JSON    json.RawMessage           `json:"json"`
+		Time    time.Time                 `json:"time"`
+		Slice   []SomeOtherType           `json:"slice"`
+		Map     map[string]*SomeOtherType `json:"map"`
+
+		Struct struct {
+			X string `json:"x"`
+		} `json:"struct"`
+
+		EmptyStruct struct {
+			Y string
+		} `json:"structWithoutFields"`
+
+		Ptr *SomeOtherType `json:"ptr"`
+	}
+
+	g := openapi3gen.NewGenerator()
+	schemaRef, err := g.NewSchemaRefForValue(&SomeStruct{}, nil)
+	if err != nil {
+		panic(err)
+	}
+
+	fmt.Printf("g.SchemaRefs: %d\n", len(g.SchemaRefs))
+	var data []byte
+	if data, err = json.MarshalIndent(&schemaRef, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemaRef: %s\n", data)
+	// Output:
+	// g.SchemaRefs: 15
+	// schemaRef: {
+	//   "properties": {
+	//     "bool": {
+	//       "type": "boolean"
+	//     },
+	//     "bytes": {
+	//       "format": "byte",
+	//       "type": "string"
+	//     },
+	//     "float64": {
+	//       "format": "double",
+	//       "type": "number"
+	//     },
+	//     "int": {
+	//       "type": "integer"
+	//     },
+	//     "int64": {
+	//       "format": "int64",
+	//       "type": "integer"
+	//     },
+	//     "json": {},
+	//     "map": {
+	//       "additionalProperties": {
+	//         "type": "string"
+	//       },
+	//       "type": "object"
+	//     },
+	//     "ptr": {
+	//       "type": "string"
+	//     },
+	//     "slice": {
+	//       "items": {
+	//         "type": "string"
+	//       },
+	//       "type": "array"
+	//     },
+	//     "string": {
+	//       "type": "string"
+	//     },
+	//     "struct": {
+	//       "properties": {
+	//         "x": {
+	//           "type": "string"
+	//         }
+	//       },
+	//       "type": "object"
+	//     },
+	//     "structWithoutFields": {},
+	//     "time": {
+	//       "format": "date-time",
+	//       "type": "string"
+	//     }
+	//   },
+	//   "type": "object"
+	// }
 }
 
-func TestCyclic(t *testing.T) {
-	schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{}, ThrowErrorOnCycle())
-	require.IsType(t, &CycleError{}, err)
-	require.Nil(t, schemaRef)
-	require.Empty(t, refsMap)
+func ExampleThrowErrorOnCycle() {
+	type CyclicType0 struct {
+		CyclicField *struct {
+			CyclicField *CyclicType0 `json:"b"`
+		} `json:"a"`
+	}
+
+	schemas := make(openapi3.Schemas)
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas, openapi3gen.ThrowErrorOnCycle())
+	if schemaRef != nil || err == nil {
+		panic(`With option ThrowErrorOnCycle, an error is returned when a schema reference cycle is found`)
+	}
+	if _, ok := err.(*openapi3gen.CycleError); !ok {
+		panic(`With option ThrowErrorOnCycle, an error of type CycleError is returned`)
+	}
+	if len(schemas) != 0 {
+		panic(`No references should have been collected at this point`)
+	}
+
+	if schemaRef, err = openapi3gen.NewSchemaRefForValue(&CyclicType0{}, schemas); err != nil {
+		panic(err)
+	}
+
+	var data []byte
+	if data, err = json.MarshalIndent(schemaRef, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemaRef: %s\n", data)
+	if data, err = json.MarshalIndent(schemas, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemas: %s\n", data)
+	// Output:
+	// schemaRef: {
+	//   "properties": {
+	//     "a": {
+	//       "properties": {
+	//         "b": {
+	//           "$ref": "#/components/schemas/CyclicType0"
+	//         }
+	//       },
+	//       "type": "object"
+	//     }
+	//   },
+	//   "type": "object"
+	// }
+	// schemas: {
+	//   "CyclicType0": {
+	//     "properties": {
+	//       "a": {
+	//         "properties": {
+	//           "b": {
+	//             "$ref": "#/components/schemas/CyclicType0"
+	//           }
+	//         },
+	//         "type": "object"
+	//       }
+	//     },
+	//     "type": "object"
+	//   }
+	// }
 }
 
 func TestExportedNonTagged(t *testing.T) {
@@ -34,7 +182,7 @@ func TestExportedNonTagged(t *testing.T) {
 		EvenAYaml  string `yaml:"even_a_yaml"`
 	}
 
-	schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields())
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields())
 	require.NoError(t, err)
 	require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{
 		Type: "object",
@@ -45,21 +193,34 @@ func TestExportedNonTagged(t *testing.T) {
 		}}}, schemaRef)
 }
 
-func TestExportUint(t *testing.T) {
+func ExampleUseAllExportedFields() {
 	type UnsignedIntStruct struct {
 		UnsignedInt uint `json:"uint"`
 	}
 
-	schemaRef, _, err := NewSchemaRefForValue(&UnsignedIntStruct{}, UseAllExportedFields())
-	require.NoError(t, err)
-	require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{
-		Type: "object",
-		Properties: map[string]*openapi3.SchemaRef{
-			"uint": {Value: &openapi3.Schema{Type: "integer", Min: &zeroInt}},
-		}}}, schemaRef)
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(&UnsignedIntStruct{}, nil, openapi3gen.UseAllExportedFields())
+	if err != nil {
+		panic(err)
+	}
+
+	var data []byte
+	if data, err = json.MarshalIndent(schemaRef, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemaRef: %s\n", data)
+	// Output:
+	// schemaRef: {
+	//   "properties": {
+	//     "uint": {
+	//       "minimum": 0,
+	//       "type": "integer"
+	//     }
+	//   },
+	//   "type": "object"
+	// }
 }
 
-func TestEmbeddedStructs(t *testing.T) {
+func ExampleGenerator_GenerateSchemaRef() {
 	type EmbeddedStruct struct {
 		ID string
 	}
@@ -76,17 +237,31 @@ func TestEmbeddedStructs(t *testing.T) {
 		},
 	}
 
-	generator := NewGenerator(UseAllExportedFields())
+	generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields())
 
 	schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
-	require.NoError(t, err)
-
-	var ok bool
-	_, ok = schemaRef.Value.Properties["Name"]
-	require.Equal(t, true, ok)
+	if err != nil {
+		panic(err)
+	}
 
-	_, ok = schemaRef.Value.Properties["ID"]
-	require.Equal(t, true, ok)
+	var data []byte
+	if data, err = json.MarshalIndent(schemaRef.Value.Properties["Name"].Value, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf(`schemaRef.Value.Properties["Name"].Value: %s`, data)
+	fmt.Println()
+	if data, err = json.MarshalIndent(schemaRef.Value.Properties["ID"].Value, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf(`schemaRef.Value.Properties["ID"].Value: %s`, data)
+	fmt.Println()
+	// Output:
+	// schemaRef.Value.Properties["Name"].Value: {
+	//   "type": "string"
+	// }
+	// schemaRef.Value.Properties["ID"].Value: {
+	//   "type": "string"
+	// }
 }
 
 func TestEmbeddedPointerStructs(t *testing.T) {
@@ -106,7 +281,7 @@ func TestEmbeddedPointerStructs(t *testing.T) {
 		},
 	}
 
-	generator := NewGenerator(UseAllExportedFields())
+	generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields())
 
 	schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
 	require.NoError(t, err)
@@ -132,7 +307,7 @@ func TestCyclicReferences(t *testing.T) {
 		MapCycle:   nil,
 	}
 
-	generator := NewGenerator(UseAllExportedFields())
+	generator := openapi3gen.NewGenerator(openapi3gen.UseAllExportedFields())
 
 	schemaRef, err := generator.GenerateSchemaRef(reflect.TypeOf(instance))
 	require.NoError(t, err)
@@ -149,7 +324,7 @@ func TestCyclicReferences(t *testing.T) {
 	require.Equal(t, "#/components/schemas/ObjectDiff", schemaRef.Value.Properties["MapCycle"].Value.AdditionalProperties.Ref)
 }
 
-func TestSchemaCustomizer(t *testing.T) {
+func ExampleSchemaCustomizer() {
 	type NestedInnerBla struct {
 		Enum1Field string `json:"enum1" myenumtag:"a,b"`
 	}
@@ -169,8 +344,7 @@ func TestSchemaCustomizer(t *testing.T) {
 		EnumField3 string `json:"enum3" myenumtag:"e,f"`
 	}
 
-	schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
-		t.Logf("Field=%s,Tag=%s", name, tag)
+	customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
 		if tag.Get("mymintag") != "" {
 			minVal, err := strconv.ParseFloat(tag.Get("mymintag"), 64)
 			if err != nil {
@@ -191,58 +365,137 @@ func TestSchemaCustomizer(t *testing.T) {
 			}
 		}
 		return nil
-	}))
-	require.NoError(t, err)
-	jsonSchema, err := json.MarshalIndent(schemaRef, "", "  ")
-	require.NoError(t, err)
-	require.JSONEq(t, `{
-  "properties": {
-    "AnonStruct": {
-      "properties": {
-        "InnerFieldWithTag": {
-          "maximum": 50,
-          "minimum": -1,
-          "type": "integer"
-        },
-        "InnerFieldWithoutTag": {
-          "type": "integer"
-        },
-				"enum1": {
-					"enum": [
-						"a",
-						"b"
-					],
-					"type": "string"
-				}
-      },
-      "type": "object"
-    },
-    "UntaggedStringField": {
-      "type": "string"
-    },
-    "enum2": {
-      "enum": [
-        "c",
-        "d"
-      ],
-      "type": "string"
-    },
-    "enum3": {
-      "enum": [
-        "e",
-        "f"
-      ],
-      "type": "string"
-    }
-  },
-  "type": "object"
-}`, string(jsonSchema))
+	})
+
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer)
+	if err != nil {
+		panic(err)
+	}
+
+	var data []byte
+	if data, err = json.MarshalIndent(schemaRef, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemaRef: %s\n", data)
+	// Output:
+	// schemaRef: {
+	//   "properties": {
+	//     "AnonStruct": {
+	//       "properties": {
+	//         "InnerFieldWithTag": {
+	//           "maximum": 50,
+	//           "minimum": -1,
+	//           "type": "integer"
+	//         },
+	//         "InnerFieldWithoutTag": {
+	//           "type": "integer"
+	//         },
+	//         "enum1": {
+	//           "enum": [
+	//             "a",
+	//             "b"
+	//           ],
+	//           "type": "string"
+	//         }
+	//       },
+	//       "type": "object"
+	//     },
+	//     "UntaggedStringField": {
+	//       "type": "string"
+	//     },
+	//     "enum2": {
+	//       "enum": [
+	//         "c",
+	//         "d"
+	//       ],
+	//       "type": "string"
+	//     },
+	//     "enum3": {
+	//       "enum": [
+	//         "e",
+	//         "f"
+	//       ],
+	//       "type": "string"
+	//     }
+	//   },
+	//   "type": "object"
+	// }
 }
 
 func TestSchemaCustomizerError(t *testing.T) {
-	type Bla struct{}
-	_, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields(), SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
+	customizer := openapi3gen.SchemaCustomizer(func(name string, ft reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error {
 		return errors.New("test error")
-	}))
+	})
+
+	type Bla struct{}
+	_, err := openapi3gen.NewSchemaRefForValue(&Bla{}, nil, openapi3gen.UseAllExportedFields(), customizer)
 	require.EqualError(t, err, "test error")
 }
+
+func ExampleNewSchemaRefForValue_recursive() {
+	type RecursiveType struct {
+		Field1     string           `json:"field1"`
+		Field2     string           `json:"field2"`
+		Field3     string           `json:"field3"`
+		Components []*RecursiveType `json:"children,omitempty"`
+	}
+
+	schemas := make(openapi3.Schemas)
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(&RecursiveType{}, schemas)
+	if err != nil {
+		panic(err)
+	}
+
+	var data []byte
+	if data, err = json.MarshalIndent(&schemas, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemas: %s\n", data)
+	if data, err = json.MarshalIndent(&schemaRef, "", "  "); err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemaRef: %s\n", data)
+	// Output:
+	// schemas: {
+	//   "RecursiveType": {
+	//     "properties": {
+	//       "children": {
+	//         "items": {
+	//           "$ref": "#/components/schemas/RecursiveType"
+	//         },
+	//         "type": "array"
+	//       },
+	//       "field1": {
+	//         "type": "string"
+	//       },
+	//       "field2": {
+	//         "type": "string"
+	//       },
+	//       "field3": {
+	//         "type": "string"
+	//       }
+	//     },
+	//     "type": "object"
+	//   }
+	// }
+	// schemaRef: {
+	//   "properties": {
+	//     "children": {
+	//       "items": {
+	//         "$ref": "#/components/schemas/RecursiveType"
+	//       },
+	//       "type": "array"
+	//     },
+	//     "field1": {
+	//       "type": "string"
+	//     },
+	//     "field2": {
+	//       "type": "string"
+	//     },
+	//     "field3": {
+	//       "type": "string"
+	//     }
+	//   },
+	//   "type": "object"
+	// }
+}
diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go
index d997e23..99e94ae 100644
--- a/openapi3gen/simple_test.go
+++ b/openapi3gen/simple_test.go
@@ -36,15 +36,11 @@ type (
 )
 
 func Example() {
-	schemaRef, refsMap, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{})
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{}, nil)
 	if err != nil {
 		panic(err)
 	}
 
-	if len(refsMap) != 15 {
-		panic(fmt.Sprintf("unintended len(refsMap) = %d", len(refsMap)))
-	}
-
 	data, err := json.MarshalIndent(schemaRef, "", "  ")
 	if err != nil {
 		panic(err)