Repository: getkin/kin-openapi (go)

Issue:
Cannot create a useable schema
I am taking the reverse road most usually take by generating openapi3 documentation from a minimal http framework on top of chi, the start was pretty fast and I can generate everything I need for the request path arguments but I am stuck trying to generate a schema for the `requestBody` of an operation, my best attempt so far is:


```go
type Pet struct {
  Id   string `json:"id"`
  Name string `json:"name"`
}

gen := openapi3gen.NewGenerator()

// bodyField points to `Pet` reflected type
bodySchema, err := gen.GenerateSchemaRef(bodyField.Type)
// error is checked but I removed it for clarity

body := openapi3.NewRequestBody()
body.Content = openapi3.NewContentWithJSONSchema(bodySchema.Value)
bodyRef := &openapi3.RequestBodyRef{
  Value: body,
}

op.RequestBody = bodyRef
swagger.AddOperation(pattern, method, op)
```

Aside from being really verbose (there may be faster way to do things but that's the best I found for now) this works but generates:

```json
{
  "content": {
    "application/json": {
      "schema": {
        "properties": {
          "id": {
            "$ref": "string"
          },
          "name": {
            "$ref": "string"
          }
        },
        "type": "object"
      }
    }
  }
}
```

this cause the viewers to try and request `<my-domain>/string` and i am not sure what I missed, from what I understand internal references should start with "#" but I see no way of influencing what ref are generated and I feel my lack of familiarity with openapi3 does not help xD

Any help or pointer in the right direction will be greatly appreciated :)

Pull request:
Add an openapi3gen example + options
Fixes #288

Patch:
diff --git a/jsoninfo/field_info.go b/jsoninfo/field_info.go
index d949a79..d2ad505 100644
--- a/jsoninfo/field_info.go
+++ b/jsoninfo/field_info.go
@@ -63,7 +63,7 @@ iteration:
 
 		// Read our custom "multijson" tag that
 		// allows multiple fields with the same name.
-		if v := f.Tag.Get("multijson"); len(v) > 0 {
+		if v := f.Tag.Get("multijson"); v != "" {
 			field.MultipleFields = true
 			jsonTag = v
 		}
@@ -74,11 +74,11 @@ iteration:
 		}
 
 		// Parse the tag
-		if len(jsonTag) > 0 {
+		if jsonTag != "" {
 			field.HasJSONTag = true
 			for i, part := range strings.Split(jsonTag, ",") {
 				if i == 0 {
-					if len(part) > 0 {
+					if part != "" {
 						field.JSONName = part
 					}
 				} else {
@@ -92,12 +92,8 @@ iteration:
 			}
 		}
 
-		if _, ok := field.Type.MethodByName("MarshalJSON"); ok {
-			field.TypeIsMarshaller = true
-		}
-		if _, ok := field.Type.MethodByName("UnmarshalJSON"); ok {
-			field.TypeIsUnmarshaller = true
-		}
+		_, field.TypeIsMarshaller = field.Type.MethodByName("MarshalJSON")
+		_, field.TypeIsUnmarshaller = field.Type.MethodByName("UnmarshalJSON")
 
 		// Field is done
 		fields = append(fields, field)
diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go
index 4a80405..4cf022e 100644
--- a/openapi3gen/openapi3gen.go
+++ b/openapi3gen/openapi3gen.go
@@ -1,8 +1,9 @@
-// Package openapi3gen generates OpenAPI 3 schemas for Go types.
+// Package openapi3gen generates OpenAPIv3 JSON schemas from Go types.
 package openapi3gen
 
 import (
 	"encoding/json"
+	"math"
 	"reflect"
 	"strings"
 	"time"
@@ -18,8 +19,22 @@ func (err *CycleError) Error() string {
 	return "Detected JSON cycle"
 }
 
-func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) {
-	g := NewGenerator()
+// Option allows tweaking SchemaRef generation
+type Option func(*generatorOpt)
+
+type generatorOpt struct {
+	useAllExportedFields bool
+}
+
+// UseAllExportedFields changes the default behavior of only
+// generating schemas for struct fields with a JSON tag.
+func UseAllExportedFields() Option {
+	return func(x *generatorOpt) { x.useAllExportedFields = true }
+}
+
+// NewSchemaRefForValue uses reflection on the given value to produce a SchemaRef.
+func NewSchemaRefForValue(value interface{}, opts ...Option) (*openapi3.SchemaRef, map[*openapi3.SchemaRef]int, error) {
+	g := NewGenerator(opts...)
 	ref, err := g.GenerateSchemaRef(reflect.TypeOf(value))
 	for ref := range g.SchemaRefs {
 		ref.Ref = ""
@@ -28,6 +43,8 @@ func NewSchemaRefForValue(value interface{}) (*openapi3.SchemaRef, map[*openapi3
 }
 
 type Generator struct {
+	opts generatorOpt
+
 	Types map[reflect.Type]*openapi3.SchemaRef
 
 	// SchemaRefs contains all references and their counts.
@@ -36,20 +53,25 @@ type Generator struct {
 	SchemaRefs map[*openapi3.SchemaRef]int
 }
 
-func NewGenerator() *Generator {
+func NewGenerator(opts ...Option) *Generator {
+	gOpt := &generatorOpt{}
+	for _, f := range opts {
+		f(gOpt)
+	}
 	return &Generator{
 		Types:      make(map[reflect.Type]*openapi3.SchemaRef),
 		SchemaRefs: make(map[*openapi3.SchemaRef]int),
+		opts:       *gOpt,
 	}
 }
 
 func (g *Generator) GenerateSchemaRef(t reflect.Type) (*openapi3.SchemaRef, error) {
+	//check generatorOpt consistency here
 	return g.generateSchemaRefFor(nil, t)
 }
 
 func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) {
-	ref := g.Types[t]
-	if ref != nil {
+	if ref := g.Types[t]; ref != nil {
 		g.SchemaRefs[ref]++
 		return ref, nil
 	}
@@ -62,7 +84,6 @@ func (g *Generator) generateSchemaRefFor(parents []*jsoninfo.TypeInfo, t reflect
 }
 
 func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflect.Type) (*openapi3.SchemaRef, error) {
-	// Get TypeInfo
 	typeInfo := jsoninfo.GetTypeInfo(t)
 	for _, parent := range parents {
 		if parent == typeInfo {
@@ -70,19 +91,15 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
 		}
 	}
 
-	// Doesn't exist.
-	// Create the schema.
 	if cap(parents) == 0 {
 		parents = make([]*jsoninfo.TypeInfo, 0, 4)
 	}
 	parents = append(parents, typeInfo)
 
-	// Ignore pointers
 	for t.Kind() == reflect.Ptr {
 		t = t.Elem()
 	}
 
-	// Create instance
 	if strings.HasSuffix(t.Name(), "Ref") {
 		_, a := t.FieldByName("Ref")
 		v, b := t.FieldByName("Value")
@@ -104,23 +121,54 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
 		}
 	}
 
-	// Allocate schema
 	schema := &openapi3.Schema{}
 
 	switch t.Kind() {
 	case reflect.Func, reflect.Chan:
-		return nil, nil
+		return nil, nil // ignore
+
 	case reflect.Bool:
 		schema.Type = "boolean"
 
-	case reflect.Int,
-		reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
-		reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+	case reflect.Int:
+		schema.Type = "integer"
+	case reflect.Int8:
+		schema.Type = "integer"
+		schema.Min = &minInt8
+		schema.Max = &maxInt8
+	case reflect.Int16:
+		schema.Type = "integer"
+		schema.Min = &minInt16
+		schema.Max = &maxInt16
+	case reflect.Int32:
+		schema.Type = "integer"
+		schema.Format = "int32"
+	case reflect.Int64:
 		schema.Type = "integer"
 		schema.Format = "int64"
+	case reflect.Uint8:
+		schema.Type = "integer"
+		schema.Min = &zeroInt
+		schema.Max = &maxUint8
+	case reflect.Uint16:
+		schema.Type = "integer"
+		schema.Min = &zeroInt
+		schema.Max = &maxUint16
+	case reflect.Uint32:
+		schema.Type = "integer"
+		schema.Min = &zeroInt
+		schema.Max = &maxUint32
+	case reflect.Uint64:
+		schema.Type = "integer"
+		schema.Min = &zeroInt
+		schema.Max = &maxUint64
 
-	case reflect.Float32, reflect.Float64:
+	case reflect.Float32:
 		schema.Type = "number"
+		schema.Format = "float"
+	case reflect.Float64:
+		schema.Type = "number"
+		schema.Format = "double"
 
 	case reflect.String:
 		schema.Type = "string"
@@ -128,9 +176,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
 	case reflect.Slice:
 		if t.Elem().Kind() == reflect.Uint8 {
 			if t == rawMessageType {
-				return &openapi3.SchemaRef{
-					Value: schema,
-				}, nil
+				return &openapi3.SchemaRef{Value: schema}, nil
 			}
 			schema.Type = "string"
 			schema.Format = "byte"
@@ -163,17 +209,26 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
 			schema.Format = "date-time"
 		} else {
 			for _, fieldInfo := range typeInfo.Fields {
-				// Only fields with JSON tag are considered
-				if !fieldInfo.HasJSONTag {
+				// Only fields with JSON tag are considered (by default)
+				if !fieldInfo.HasJSONTag && !g.opts.useAllExportedFields {
 					continue
 				}
-				ref, err := g.generateSchemaRefFor(parents, fieldInfo.Type)
+				// If asked, try to use yaml tag
+				name, fType := fieldInfo.JSONName, fieldInfo.Type
+				if !fieldInfo.HasJSONTag && g.opts.useAllExportedFields {
+					ff := t.Field(fieldInfo.Index[len(fieldInfo.Index)-1])
+					if tag, ok := ff.Tag.Lookup("yaml"); ok && tag != "-" {
+						name, fType = tag, ff.Type
+					}
+				}
+
+				ref, err := g.generateSchemaRefFor(parents, fType)
 				if err != nil {
 					return nil, err
 				}
 				if ref != nil {
 					g.SchemaRefs[ref]++
-					schema.WithPropertyRef(fieldInfo.JSONName, ref)
+					schema.WithPropertyRef(name, ref)
 				}
 			}
 
@@ -183,6 +238,7 @@ func (g *Generator) generateWithoutSaving(parents []*jsoninfo.TypeInfo, t reflec
 			}
 		}
 	}
+
 	return openapi3.NewSchemaRef(t.Name(), schema), nil
 }
 
@@ -192,4 +248,14 @@ var RefSchemaRef = openapi3.NewSchemaRef("Ref",
 var (
 	timeType       = reflect.TypeOf(time.Time{})
 	rawMessageType = reflect.TypeOf(json.RawMessage{})
+
+	zeroInt   = float64(0)
+	maxInt8   = float64(math.MaxInt8)
+	minInt8   = float64(math.MinInt8)
+	maxInt16  = float64(math.MaxInt16)
+	minInt16  = float64(math.MinInt16)
+	maxUint8  = float64(math.MaxUint8)
+	maxUint16 = float64(math.MaxUint16)
+	maxUint32 = float64(math.MaxUint32)
+	maxUint64 = float64(math.MaxUint64)
 )


Test patch:
diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go
index 2a58433..1422017 100644
--- a/openapi3gen/openapi3gen_test.go
+++ b/openapi3gen/openapi3gen_test.go
@@ -1,10 +1,9 @@
 package openapi3gen
 
 import (
-	"encoding/json"
 	"testing"
-	"time"
 
+	"github.com/getkin/kin-openapi/openapi3"
 	"github.com/stretchr/testify/require"
 )
 
@@ -16,96 +15,27 @@ type CyclicType1 struct {
 }
 
 func TestCyclic(t *testing.T) {
-	schema, refsMap, err := NewSchemaRefForValue(&CyclicType0{})
+	schemaRef, refsMap, err := NewSchemaRefForValue(&CyclicType0{})
 	require.IsType(t, &CycleError{}, err)
-	require.Nil(t, schema)
+	require.Nil(t, schemaRef)
 	require.Empty(t, refsMap)
 }
 
-func TestSimple(t *testing.T) {
-	type ExampleChild string
-	type Example 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   []*ExampleChild          `json:"slice"`
-		Map     map[string]*ExampleChild `json:"map"`
-		Struct  struct {
-			X string `json:"x"`
-		} `json:"struct"`
-		EmptyStruct struct {
-			X string
-		} `json:"structWithoutFields"`
-		Ptr *ExampleChild `json:"ptr"`
+func TestExportedNonTagged(t *testing.T) {
+	type Bla struct {
+		A          string
+		Another    string `json:"another"`
+		yetAnother string
+		EvenAYaml  string `yaml:"even_a_yaml"`
 	}
 
-	schema, refsMap, err := NewSchemaRefForValue(&Example{})
+	schemaRef, _, err := NewSchemaRefForValue(&Bla{}, UseAllExportedFields())
 	require.NoError(t, err)
-	require.Len(t, refsMap, 14)
-	data, err := json.Marshal(schema)
-	require.NoError(t, err)
-	require.JSONEq(t, expectedSimple, string(data))
-}
-
-const expectedSimple = `
-{
-  "type": "object",
-  "properties": {
-    "bool": {
-      "type": "boolean"
-    },
-    "int": {
-      "type": "integer",
-      "format": "int64"
-    },
-    "int64": {
-      "type": "integer",
-      "format": "int64"
-    },
-    "float64": {
-      "type": "number"
-    },
-    "time": {
-      "type": "string",
-      "format": "date-time"
-    },
-    "string": {
-      "type": "string"
-    },
-    "bytes": {
-      "type": "string",
-      "format": "byte"
-    },
-    "json": {},
-    "slice": {
-      "type": "array",
-      "items": {
-        "type": "string"
-      }
-    },
-    "map": {
-      "type": "object",
-      "additionalProperties": {
-        "type": "string"
-      }
-    },
-    "struct": {
-      "type": "object",
-      "properties": {
-        "x": {
-          "type": "string"
-        }
-      }
-    },
-    "structWithoutFields": {},
-    "ptr": {
-      "type": "string"
-    }
-  }
+	require.Equal(t, &openapi3.SchemaRef{Value: &openapi3.Schema{
+		Type: "object",
+		Properties: map[string]*openapi3.SchemaRef{
+			"A":           {Value: &openapi3.Schema{Type: "string"}},
+			"another":     {Value: &openapi3.Schema{Type: "string"}},
+			"even_a_yaml": {Value: &openapi3.Schema{Type: "string"}},
+		}}}, schemaRef)
 }
-`
diff --git a/openapi3gen/simple_test.go b/openapi3gen/simple_test.go
new file mode 100644
index 0000000..d997e23
--- /dev/null
+++ b/openapi3gen/simple_test.go
@@ -0,0 +1,109 @@
+package openapi3gen_test
+
+import (
+	"encoding/json"
+	"fmt"
+	"time"
+
+	"github.com/getkin/kin-openapi/openapi3gen"
+)
+
+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"`
+	}
+
+	SomeOtherType string
+)
+
+func Example() {
+	schemaRef, refsMap, err := openapi3gen.NewSchemaRefForValue(&SomeStruct{})
+	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)
+	}
+	fmt.Printf("%s\n", data)
+	// Output:
+	// {
+	//   "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"
+	// }
+}