Repository: getkin/kin-openapi
Pull request #931 (2024-04-05 20:14:50)

openapi3: implement YAML Marshaller interface for paths and responses
Support marshaling using [pkg.go.dev/gopkg.in/yaml.v3](https://pkg.go.dev/gopkg.in/yaml.v3)

Related to https://github.com/getkin/kin-openapi/issues/907.


Patch:
diff --git a/.github/docs/openapi3.txt b/.github/docs/openapi3.txt
index 1ae2093..8584f96 100644
--- a/.github/docs/openapi3.txt
+++ b/.github/docs/openapi3.txt
@@ -963,6 +963,9 @@ func (paths *Paths) Map() (m map[string]*PathItem)
 func (paths *Paths) MarshalJSON() ([]byte, error)
     MarshalJSON returns the JSON encoding of Paths.
 
+func (paths *Paths) MarshalYAML() (any, error)
+    Support YAML Marshaler interface for gopkg.in/yaml
+
 func (paths *Paths) Set(key string, value *PathItem)
     Set adds or replaces key 'key' of 'paths' with 'value'. Note: 'paths' MUST
     be non-nil
@@ -1174,6 +1177,9 @@ func (responses *Responses) Map() (m map[string]*ResponseRef)
 func (responses *Responses) MarshalJSON() ([]byte, error)
     MarshalJSON returns the JSON encoding of Responses.
 
+func (responses *Responses) MarshalYAML() (any, error)
+    Support YAML Marshaler interface for gopkg.in/yaml
+
 func (responses *Responses) Set(key string, value *ResponseRef)
     Set adds or replaces key 'key' of 'responses' with 'value'. Note:
     'responses' MUST be non-nil
diff --git a/.github/docs/openapi3gen.txt b/.github/docs/openapi3gen.txt
index 9cf293c..e692b85 100644
--- a/.github/docs/openapi3gen.txt
+++ b/.github/docs/openapi3gen.txt
@@ -70,3 +70,10 @@ type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag,
     A SchemaCustomizerFn can return an ExcludeSchemaSentinel error to indicate
     that the schema for this field should not be included in the final output
 
+type SetSchemar interface {
+	SetSchema(*openapi3.Schema)
+}
+    SetSchemar allows client to set their own schema definition according to
+    their specification. Useful when some custom datatype is needed and/or some
+    custom logic is needed on how the schema values would be generated
+
diff --git a/openapi3/paths.go b/openapi3/paths.go
index ac4f58b..daafe71 100644
--- a/openapi3/paths.go
+++ b/openapi3/paths.go
@@ -218,6 +218,21 @@ func (paths *Paths) validateUniqueOperationIDs() error {
 	return nil
 }
 
+// Support YAML Marshaler interface for gopkg.in/yaml
+func (paths *Paths) MarshalYAML() (any, error) {
+	res := make(map[string]any, len(paths.Extensions)+len(paths.m))
+
+	for k, v := range paths.Extensions {
+		res[k] = v
+	}
+
+	for k, v := range paths.m {
+		res[k] = v
+	}
+
+	return res, nil
+}
+
 func normalizeTemplatedPath(path string) (string, uint, map[string]struct{}) {
 	if strings.IndexByte(path, '{') < 0 {
 		return path, 0, nil
diff --git a/openapi3/response.go b/openapi3/response.go
index d8b0472..f69c237 100644
--- a/openapi3/response.go
+++ b/openapi3/response.go
@@ -98,6 +98,21 @@ func (responses *Responses) Validate(ctx context.Context, opts ...ValidationOpti
 	return validateExtensions(ctx, responses.Extensions)
 }
 
+// Support YAML Marshaler interface for gopkg.in/yaml
+func (responses *Responses) MarshalYAML() (any, error) {
+	res := make(map[string]any, len(responses.Extensions)+len(responses.m))
+
+	for k, v := range responses.Extensions {
+		res[k] = v
+	}
+
+	for k, v := range responses.m {
+		res[k] = v
+	}
+
+	return res, nil
+}
+
 // Response is specified by OpenAPI/Swagger 3.0 standard.
 // See https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#response-object
 type Response struct {
diff --git a/openapi3gen/openapi3gen.go b/openapi3gen/openapi3gen.go
index 95805ee..a301337 100644
--- a/openapi3gen/openapi3gen.go
+++ b/openapi3gen/openapi3gen.go
@@ -35,6 +35,13 @@ type Option func(*generatorOpt)
 // the final output
 type SchemaCustomizerFn func(name string, t reflect.Type, tag reflect.StructTag, schema *openapi3.Schema) error
 
+// SetSchemar allows client to set their own schema definition according to
+// their specification. Useful when some custom datatype is needed and/or some custom logic
+// is needed on how the schema values would be generated
+type SetSchemar interface {
+	SetSchema(*openapi3.Schema)
+}
+
 type generatorOpt struct {
 	useAllExportedFields bool
 	throwErrorOnCycle    bool
@@ -347,6 +354,14 @@ func (g *Generator) generateWithoutSaving(parents []*theTypeInfo, t reflect.Type
 				schema.Type = &openapi3.Types{"object"}
 			}
 		}
+
+	default:
+		// Object has their own schema's implementation, so we'll use those
+		if v := reflect.New(t); v.CanInterface() {
+			if v, ok := v.Interface().(SetSchemar); ok {
+				v.SetSchema(schema)
+			}
+		}
 	}
 
 	if g.opts.schemaCustomizer != nil {


Test patch:
diff --git a/openapi3gen/openapi3gen_test.go b/openapi3gen/openapi3gen_test.go
index daea9ff..8263983 100644
--- a/openapi3gen/openapi3gen_test.go
+++ b/openapi3gen/openapi3gen_test.go
@@ -590,3 +590,43 @@ func ExampleNewSchemaRefForValue_recursive() {
 	//   "type": "object"
 	// }
 }
+
+type ID [16]byte
+
+// T implements SetSchemar, allowing it to set an OpenAPI schema.
+type T struct {
+	ID ID `json:"id"`
+}
+
+func (_ *ID) SetSchema(schema *openapi3.Schema) {
+	schema.Type = &openapi3.Types{"string"} // Assuming this matches your custom implementation
+	schema.Format = "uuid"
+}
+
+func ExampleSetSchemar() {
+	schemas := make(openapi3.Schemas)
+	instance := &T{
+		ID: ID{},
+	}
+
+	// Generate the schema for the instance
+	schemaRef, err := openapi3gen.NewSchemaRefForValue(instance, schemas)
+	if err != nil {
+		panic(err)
+	}
+	data, err := json.MarshalIndent(schemaRef, "", "  ")
+	if err != nil {
+		panic(err)
+	}
+	fmt.Printf("schemaRef: %s\n", data)
+	// Output:
+	// schemaRef: {
+	//   "properties": {
+	//     "id": {
+	//       "format": "uuid",
+	//       "type": "string"
+	//     }
+	//   },
+	//   "type": "object"
+	// }
+}