Repository: ogen-go/ogen
Pull request #490 (2022-07-18 15:18:28)

fix: implement `yaml.Marshaler` for types with custom unmarshaler


Patch:
diff --git a/jsonschema/raw_custom.go b/jsonschema/raw_custom.go
new file mode 100644
index 00000000..b652bbba
--- /dev/null
+++ b/jsonschema/raw_custom.go
@@ -0,0 +1,260 @@
+package jsonschema
+
+import (
+	"encoding/json"
+	"reflect"
+
+	"github.com/go-faster/errors"
+	"github.com/go-faster/jx"
+	yaml "github.com/go-faster/yamlx"
+)
+
+// RawProperty is item of RawProperties.
+type RawProperty struct {
+	Name   string
+	Schema *RawSchema
+}
+
+// RawProperties is unparsed JSON Schema properties validator description.
+type RawProperties []RawProperty
+
+// MarshalYAML implements yaml.Marshaler.
+func (p RawProperties) MarshalYAML() (interface{}, error) {
+	content := make([]*yaml.Node, 0, len(p)*2)
+	for _, prop := range p {
+		var val yaml.Node
+		if err := val.Encode(prop.Schema); err != nil {
+			return nil, err
+		}
+		content = append(content,
+			&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: prop.Name},
+			&val,
+		)
+	}
+
+	return &yaml.Node{
+		Kind:    yaml.MappingNode,
+		Content: content,
+	}, nil
+}
+
+// UnmarshalYAML implements yaml.Unmarshaler.
+func (p *RawProperties) UnmarshalYAML(node *yaml.Node) error {
+	if node.Kind != yaml.MappingNode {
+		return &yaml.UnmarshalError{
+			Node: node,
+			Type: reflect.TypeOf(p),
+			Err:  errors.Errorf("cannot unmarshal %s into %T", node.ShortTag(), p),
+		}
+	}
+	for i := 0; i < len(node.Content); i += 2 {
+		var (
+			key    = node.Content[i]
+			value  = node.Content[i+1]
+			schema *RawSchema
+		)
+		if err := value.Decode(&schema); err != nil {
+			return err
+		}
+		*p = append(*p, RawProperty{
+			Name:   key.Value,
+			Schema: schema,
+		})
+	}
+	return nil
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p RawProperties) MarshalJSON() ([]byte, error) {
+	e := jx.GetEncoder()
+	defer jx.PutEncoder(e)
+
+	e.ObjStart()
+	for _, prop := range p {
+		e.FieldStart(prop.Name)
+		b, err := json.Marshal(prop.Schema)
+		if err != nil {
+			return nil, errors.Wrap(err, "marshal")
+		}
+		e.Raw(b)
+	}
+	e.ObjEnd()
+	return e.Bytes(), nil
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (p *RawProperties) UnmarshalJSON(data []byte) error {
+	d := jx.DecodeBytes(data)
+	return d.Obj(func(d *jx.Decoder, key string) error {
+		s := new(RawSchema)
+		b, err := d.Raw()
+		if err != nil {
+			return err
+		}
+
+		if err := json.Unmarshal(b, s); err != nil {
+			return err
+		}
+
+		*p = append(*p, RawProperty{
+			Name:   key,
+			Schema: s,
+		})
+		return nil
+	})
+}
+
+// AdditionalProperties is JSON Schema additionalProperties validator description.
+type AdditionalProperties struct {
+	Bool   *bool
+	Schema RawSchema
+}
+
+// MarshalYAML implements yaml.Marshaler.
+func (p AdditionalProperties) MarshalYAML() (interface{}, error) {
+	if p.Bool != nil {
+		return *p.Bool, nil
+	}
+	return p.Schema, nil
+}
+
+// UnmarshalYAML implements yaml.Unmarshaler.
+func (p *AdditionalProperties) UnmarshalYAML(node *yaml.Node) error {
+	switch node.Kind {
+	case yaml.ScalarNode:
+		return node.Decode(&p.Bool)
+	case yaml.MappingNode:
+		return node.Decode(&p.Schema)
+	default:
+		return errors.Errorf("unexpected YAML kind %v", node.Kind)
+	}
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p AdditionalProperties) MarshalJSON() ([]byte, error) {
+	if p.Bool != nil {
+		return json.Marshal(p.Bool)
+	}
+	return json.Marshal(p.Schema)
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (p *AdditionalProperties) UnmarshalJSON(data []byte) error {
+	d := jx.DecodeBytes(data)
+	switch tt := d.Next(); tt {
+	case jx.Object:
+	case jx.Bool:
+		val, err := d.Bool()
+		if err != nil {
+			return err
+		}
+		p.Bool = &val
+		return nil
+	default:
+		return errors.Errorf("unexpected type %s", tt.String())
+	}
+
+	s := RawSchema{}
+	b, err := d.Raw()
+	if err != nil {
+		return err
+	}
+	if err := json.Unmarshal(b, &s); err != nil {
+		return err
+	}
+	p.Schema = s
+	return nil
+}
+
+// RawPatternProperty is item of RawPatternProperties.
+type RawPatternProperty struct {
+	Pattern string
+	Schema  *RawSchema
+}
+
+// RawPatternProperties is unparsed JSON Schema patternProperties validator description.
+type RawPatternProperties []RawPatternProperty
+
+// MarshalYAML implements yaml.Marshaler.
+func (p RawPatternProperties) MarshalYAML() (interface{}, error) {
+	content := make([]*yaml.Node, 0, len(p)*2)
+	for _, prop := range p {
+		var val yaml.Node
+		if err := val.Encode(prop.Schema); err != nil {
+			return nil, err
+		}
+		content = append(content,
+			&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: prop.Pattern},
+			&val,
+		)
+	}
+
+	return &yaml.Node{
+		Kind:    yaml.MappingNode,
+		Content: content,
+	}, nil
+}
+
+// UnmarshalYAML implements yaml.Unmarshaler.
+func (p *RawPatternProperties) UnmarshalYAML(node *yaml.Node) error {
+	if node.Kind != yaml.MappingNode {
+		return &yaml.UnmarshalError{
+			Node: node,
+			Type: reflect.TypeOf(p),
+			Err:  errors.Errorf("cannot unmarshal %s into %T", node.ShortTag(), p),
+		}
+	}
+	for i := 0; i < len(node.Content); i += 2 {
+		var (
+			key    = node.Content[i]
+			value  = node.Content[i+1]
+			schema *RawSchema
+		)
+		if err := value.Decode(&schema); err != nil {
+			return err
+		}
+		*p = append(*p, RawPatternProperty{
+			Pattern: key.Value,
+			Schema:  schema,
+		})
+	}
+	return nil
+}
+
+// MarshalJSON implements json.Marshaler.
+func (p RawPatternProperties) MarshalJSON() ([]byte, error) {
+	var e jx.Encoder
+	e.ObjStart()
+	for _, prop := range p {
+		e.FieldStart(prop.Pattern)
+		b, err := json.Marshal(prop.Schema)
+		if err != nil {
+			return nil, errors.Wrap(err, "marshal")
+		}
+		e.Raw(b)
+	}
+	e.ObjEnd()
+	return e.Bytes(), nil
+}
+
+// UnmarshalJSON implements json.Unmarshaler.
+func (p *RawPatternProperties) UnmarshalJSON(data []byte) error {
+	d := jx.DecodeBytes(data)
+	return d.Obj(func(d *jx.Decoder, key string) error {
+		s := new(RawSchema)
+		b, err := d.Raw()
+		if err != nil {
+			return err
+		}
+
+		if err := json.Unmarshal(b, s); err != nil {
+			return err
+		}
+
+		*p = append(*p, RawPatternProperty{
+			Pattern: key,
+			Schema:  s,
+		})
+		return nil
+	})
+}
diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go
index 17bbda0d..90d040fb 100644
--- a/jsonschema/raw_schema.go
+++ b/jsonschema/raw_schema.go
@@ -3,10 +3,6 @@ package jsonschema
 import (
 	"encoding/json"
 
-	"github.com/go-faster/errors"
-	"github.com/go-faster/jx"
-	yaml "github.com/go-faster/yamlx"
-
 	"github.com/ogen-go/ogen/internal/location"
 )
 
@@ -53,200 +49,6 @@ type RawSchema struct {
 	Locator      location.Locator           `json:"-" yaml:",inline"`
 }
 
-// RawProperty is item of RawProperties.
-type RawProperty struct {
-	Name   string
-	Schema *RawSchema
-}
-
-// RawProperties is unparsed JSON Schema properties validator description.
-type RawProperties []RawProperty
-
-// UnmarshalYAML implements yaml.Unmarshaler.
-func (p *RawProperties) UnmarshalYAML(node *yaml.Node) error {
-	if node.Kind != yaml.MappingNode {
-		return errors.Errorf("unexpected YAML kind %v", node.Kind)
-	}
-	for i := 0; i < len(node.Content); i += 2 {
-		var (
-			key    = node.Content[i]
-			value  = node.Content[i+1]
-			schema *RawSchema
-		)
-		if err := value.Decode(&schema); err != nil {
-			return err
-		}
-		*p = append(*p, RawProperty{
-			Name:   key.Value,
-			Schema: schema,
-		})
-	}
-	return nil
-}
-
-// MarshalJSON implements json.Marshaler.
-func (p RawProperties) MarshalJSON() ([]byte, error) {
-	e := jx.GetEncoder()
-	defer jx.PutEncoder(e)
-
-	e.ObjStart()
-	for _, prop := range p {
-		e.FieldStart(prop.Name)
-		b, err := json.Marshal(prop.Schema)
-		if err != nil {
-			return nil, errors.Wrap(err, "marshal")
-		}
-		e.Raw(b)
-	}
-	e.ObjEnd()
-	return e.Bytes(), nil
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (p *RawProperties) UnmarshalJSON(data []byte) error {
-	d := jx.DecodeBytes(data)
-	return d.Obj(func(d *jx.Decoder, key string) error {
-		s := new(RawSchema)
-		b, err := d.Raw()
-		if err != nil {
-			return err
-		}
-
-		if err := json.Unmarshal(b, s); err != nil {
-			return err
-		}
-
-		*p = append(*p, RawProperty{
-			Name:   key,
-			Schema: s,
-		})
-		return nil
-	})
-}
-
-// AdditionalProperties is JSON Schema additionalProperties validator description.
-type AdditionalProperties struct {
-	Bool   *bool
-	Schema RawSchema
-}
-
-// UnmarshalYAML implements yaml.Unmarshaler.
-func (p *AdditionalProperties) UnmarshalYAML(node *yaml.Node) error {
-	switch node.Kind {
-	case yaml.ScalarNode:
-		return node.Decode(&p.Bool)
-	case yaml.MappingNode:
-		return node.Decode(&p.Schema)
-	default:
-		return errors.Errorf("unexpected YAML kind %v", node.Kind)
-	}
-}
-
-// MarshalJSON implements json.Marshaler.
-func (p AdditionalProperties) MarshalJSON() ([]byte, error) {
-	if p.Bool != nil {
-		return json.Marshal(p.Bool)
-	}
-	return json.Marshal(p.Schema)
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (p *AdditionalProperties) UnmarshalJSON(data []byte) error {
-	d := jx.DecodeBytes(data)
-	switch tt := d.Next(); tt {
-	case jx.Object:
-	case jx.Bool:
-		val, err := d.Bool()
-		if err != nil {
-			return err
-		}
-		p.Bool = &val
-		return nil
-	default:
-		return errors.Errorf("unexpected type %s", tt.String())
-	}
-
-	s := RawSchema{}
-	b, err := d.Raw()
-	if err != nil {
-		return err
-	}
-	if err := json.Unmarshal(b, &s); err != nil {
-		return err
-	}
-	p.Schema = s
-	return nil
-}
-
-// RawPatternProperty is item of RawPatternProperties.
-type RawPatternProperty struct {
-	Pattern string
-	Schema  *RawSchema
-}
-
-// RawPatternProperties is unparsed JSON Schema patternProperties validator description.
-type RawPatternProperties []RawPatternProperty
-
-// UnmarshalYAML implements yaml.Unmarshaler.
-func (p *RawPatternProperties) UnmarshalYAML(node *yaml.Node) error {
-	if node.Kind != yaml.MappingNode {
-		return errors.Errorf("unexpected YAML kind %v", node.Kind)
-	}
-	for i := 0; i < len(node.Content); i += 2 {
-		var (
-			key    = node.Content[i]
-			value  = node.Content[i+1]
-			schema *RawSchema
-		)
-		if err := value.Decode(&schema); err != nil {
-			return err
-		}
-		*p = append(*p, RawPatternProperty{
-			Pattern: key.Value,
-			Schema:  schema,
-		})
-	}
-	return nil
-}
-
-// MarshalJSON implements json.Marshaler.
-func (p RawPatternProperties) MarshalJSON() ([]byte, error) {
-	var e jx.Encoder
-	e.ObjStart()
-	for _, prop := range p {
-		e.FieldStart(prop.Pattern)
-		b, err := json.Marshal(prop.Schema)
-		if err != nil {
-			return nil, errors.Wrap(err, "marshal")
-		}
-		e.Raw(b)
-	}
-	e.ObjEnd()
-	return e.Bytes(), nil
-}
-
-// UnmarshalJSON implements json.Unmarshaler.
-func (p *RawPatternProperties) UnmarshalJSON(data []byte) error {
-	d := jx.DecodeBytes(data)
-	return d.Obj(func(d *jx.Decoder, key string) error {
-		s := new(RawSchema)
-		b, err := d.Raw()
-		if err != nil {
-			return err
-		}
-
-		if err := json.Unmarshal(b, s); err != nil {
-			return err
-		}
-
-		*p = append(*p, RawPatternProperty{
-			Pattern: key,
-			Schema:  s,
-		})
-		return nil
-	})
-}
-
 // Discriminator discriminates types for OneOf, AllOf, AnyOf.
 //
 // See https://spec.openapis.org/oas/v3.1.0#discriminator-object.
diff --git a/schema.go b/schema.go
index 21410bb7..de96cf56 100644
--- a/schema.go
+++ b/schema.go
@@ -2,6 +2,7 @@ package ogen
 
 import (
 	"encoding/json"
+	"reflect"
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
@@ -250,10 +251,34 @@ type Property struct {
 // Properties is unparsed JSON Schema properties validator description.
 type Properties []Property
 
+// MarshalYAML implements yaml.Marshaler.
+func (p Properties) MarshalYAML() (interface{}, error) {
+	content := make([]*yaml.Node, 0, len(p)*2)
+	for _, prop := range p {
+		var val yaml.Node
+		if err := val.Encode(prop.Schema); err != nil {
+			return nil, err
+		}
+		content = append(content,
+			&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: prop.Name},
+			&val,
+		)
+	}
+
+	return &yaml.Node{
+		Kind:    yaml.MappingNode,
+		Content: content,
+	}, nil
+}
+
 // UnmarshalYAML implements yaml.Unmarshaler.
 func (p *Properties) UnmarshalYAML(node *yaml.Node) error {
 	if node.Kind != yaml.MappingNode {
-		return errors.Errorf("unexpected YAML kind %v", node.Kind)
+		return &yaml.UnmarshalError{
+			Node: node,
+			Type: reflect.TypeOf(p),
+			Err:  errors.Errorf("cannot unmarshal %s into %T", node.ShortTag(), p),
+		}
 	}
 	for i := 0; i < len(node.Content); i += 2 {
 		var (
@@ -318,6 +343,14 @@ type AdditionalProperties struct {
 	Schema Schema
 }
 
+// MarshalYAML implements yaml.Marshaler.
+func (p AdditionalProperties) MarshalYAML() (interface{}, error) {
+	if p.Bool != nil {
+		return *p.Bool, nil
+	}
+	return p.Schema, nil
+}
+
 // UnmarshalYAML implements yaml.Unmarshaler.
 func (p *AdditionalProperties) UnmarshalYAML(node *yaml.Node) error {
 	switch node.Kind {
@@ -375,10 +408,34 @@ type PatternProperty struct {
 // PatternProperties is unparsed JSON Schema patternProperties validator description.
 type PatternProperties []PatternProperty
 
+// MarshalYAML implements yaml.Marshaler.
+func (p PatternProperties) MarshalYAML() (interface{}, error) {
+	content := make([]*yaml.Node, 0, len(p)*2)
+	for _, prop := range p {
+		var val yaml.Node
+		if err := val.Encode(prop.Schema); err != nil {
+			return nil, err
+		}
+		content = append(content,
+			&yaml.Node{Kind: yaml.ScalarNode, Tag: "!!str", Value: prop.Pattern},
+			&val,
+		)
+	}
+
+	return &yaml.Node{
+		Kind:    yaml.MappingNode,
+		Content: content,
+	}, nil
+}
+
 // UnmarshalYAML implements yaml.Unmarshaler.
 func (p *PatternProperties) UnmarshalYAML(node *yaml.Node) error {
 	if node.Kind != yaml.MappingNode {
-		return errors.Errorf("unexpected YAML kind %v", node.Kind)
+		return &yaml.UnmarshalError{
+			Node: node,
+			Type: reflect.TypeOf(p),
+			Err:  errors.Errorf("cannot unmarshal %s into %T", node.ShortTag(), p),
+		}
 	}
 	for i := 0; i < len(node.Content); i += 2 {
 		var (


Test patch:
diff --git a/jsonschema/raw_schema_test.go b/jsonschema/raw_custom_test.go
similarity index 52%
rename from jsonschema/raw_schema_test.go
rename to jsonschema/raw_custom_test.go
index f6771e49..a93de3a9 100644
--- a/jsonschema/raw_schema_test.go
+++ b/jsonschema/raw_custom_test.go
@@ -9,13 +9,79 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+type encoding struct {
+	marshal   func(interface{}) ([]byte, error)
+	unmarshal func([]byte, interface{}) error
+	compare   func(a *require.Assertions, got, want string, msgArgs ...interface{})
+}
+
+func testCustomEncoding(
+	createVal func() interface{},
+	input string,
+	wantErr bool,
+	e encoding,
+) func(t *testing.T) {
+	return func(t *testing.T) {
+		a := require.New(t)
+
+		val := createVal()
+		err := e.unmarshal([]byte(input), val)
+		if wantErr {
+			a.Error(err)
+			t.Logf("Input: %q", input)
+			t.Logf("Error: %+v", err)
+			return
+		}
+		a.NoError(err)
+
+		data, err := e.marshal(val)
+		a.NoError(err)
+		e.compare(a, input, string(data))
+	}
+}
+
+func testCustomEncodings(
+	createVal func() interface{},
+	input string,
+	wantErr bool,
+) func(t *testing.T) {
+	js := encoding{
+		marshal:   json.Marshal,
+		unmarshal: json.Unmarshal,
+		compare:   (*require.Assertions).JSONEq,
+	}
+	yml := encoding{
+		marshal:   yaml.Marshal,
+		unmarshal: yaml.Unmarshal,
+		compare:   (*require.Assertions).YAMLEq,
+	}
+
+	return func(t *testing.T) {
+		t.Run("YAML", testCustomEncoding(
+			createVal,
+			input,
+			wantErr,
+			yml,
+		))
+		t.Run("JSON", testCustomEncoding(
+			createVal,
+			input,
+			wantErr,
+			js,
+		))
+	}
+}
+
 func TestRawProperties(t *testing.T) {
+	create := func() interface{} {
+		return &RawProperties{}
+	}
+
 	tests := []struct {
 		data    string
 		value   RawProperties
 		wantErr bool
 	}{
-
 		{`{"foo":{"type":"string"}, "bar":{"type":"number"}}`, RawProperties{
 			{Name: "foo", Schema: &RawSchema{Type: "string"}},
 			{Name: "bar", Schema: &RawSchema{Type: "number"}},
@@ -29,27 +95,15 @@ func TestRawProperties(t *testing.T) {
 	}
 	for i, tt := range tests {
 		tt := tt
-		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
-			a := require.New(t)
-
-			var val RawProperties
-			err := yaml.Unmarshal([]byte(tt.data), &val)
-			if tt.wantErr {
-				a.Error(err)
-				t.Log("Input:", tt.data)
-				t.Log("Error:", err)
-				return
-			}
-			a.NoError(err)
-
-			data, err := json.Marshal(val)
-			a.NoError(err)
-			a.JSONEq(tt.data, string(data))
-		})
+		t.Run(fmt.Sprintf("Test%d", i+1), testCustomEncodings(create, tt.data, tt.wantErr))
 	}
 }
 
 func TestAdditionalProperties(t *testing.T) {
+	create := func() interface{} {
+		return &AdditionalProperties{}
+	}
+
 	tests := []struct {
 		data    string
 		value   AdditionalProperties
@@ -67,27 +121,15 @@ func TestAdditionalProperties(t *testing.T) {
 	}
 	for i, tt := range tests {
 		tt := tt
-		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
-			a := require.New(t)
-
-			var val AdditionalProperties
-			err := yaml.Unmarshal([]byte(tt.data), &val)
-			if tt.wantErr {
-				a.Error(err)
-				t.Log("Input:", tt.data)
-				t.Log("Error:", err)
-				return
-			}
-			a.NoError(err)
-
-			data, err := json.Marshal(val)
-			a.NoError(err)
-			a.JSONEq(tt.data, string(data))
-		})
+		t.Run(fmt.Sprintf("Test%d", i+1), testCustomEncodings(create, tt.data, tt.wantErr))
 	}
 }
 
-func TestRawPatternProperties(t *testing.T) {
+func TestPatternProperties(t *testing.T) {
+	create := func() interface{} {
+		return &RawPatternProperties{}
+	}
+
 	tests := []struct {
 		data    string
 		value   RawPatternProperties
@@ -106,22 +148,6 @@ func TestRawPatternProperties(t *testing.T) {
 	}
 	for i, tt := range tests {
 		tt := tt
-		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
-			a := require.New(t)
-
-			var val RawPatternProperties
-			err := yaml.Unmarshal([]byte(tt.data), &val)
-			if tt.wantErr {
-				a.Error(err)
-				t.Log("Input:", tt.data)
-				t.Log("Error:", err)
-				return
-			}
-			a.NoError(err)
-
-			data, err := json.Marshal(val)
-			a.NoError(err)
-			a.JSONEq(tt.data, string(data))
-		})
+		t.Run(fmt.Sprintf("Test%d", i+1), testCustomEncodings(create, tt.data, tt.wantErr))
 	}
 }
diff --git a/schema_test.go b/schema_test.go
index 44afd481..707c61b6 100644
--- a/schema_test.go
+++ b/schema_test.go
@@ -9,13 +9,79 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+type encoding struct {
+	marshal   func(interface{}) ([]byte, error)
+	unmarshal func([]byte, interface{}) error
+	compare   func(a *require.Assertions, got, want string, msgArgs ...interface{})
+}
+
+func testCustomEncoding(
+	createVal func() interface{},
+	input string,
+	wantErr bool,
+	e encoding,
+) func(t *testing.T) {
+	return func(t *testing.T) {
+		a := require.New(t)
+
+		val := createVal()
+		err := e.unmarshal([]byte(input), val)
+		if wantErr {
+			a.Error(err)
+			t.Logf("Input: %q", input)
+			t.Logf("Error: %+v", err)
+			return
+		}
+		a.NoError(err)
+
+		data, err := e.marshal(val)
+		a.NoError(err)
+		e.compare(a, input, string(data))
+	}
+}
+
+func testCustomEncodings(
+	createVal func() interface{},
+	input string,
+	wantErr bool,
+) func(t *testing.T) {
+	js := encoding{
+		marshal:   json.Marshal,
+		unmarshal: json.Unmarshal,
+		compare:   (*require.Assertions).JSONEq,
+	}
+	yml := encoding{
+		marshal:   yaml.Marshal,
+		unmarshal: yaml.Unmarshal,
+		compare:   (*require.Assertions).YAMLEq,
+	}
+
+	return func(t *testing.T) {
+		t.Run("YAML", testCustomEncoding(
+			createVal,
+			input,
+			wantErr,
+			yml,
+		))
+		t.Run("JSON", testCustomEncoding(
+			createVal,
+			input,
+			wantErr,
+			js,
+		))
+	}
+}
+
 func TestProperties(t *testing.T) {
+	create := func() interface{} {
+		return &Properties{}
+	}
+
 	tests := []struct {
 		data    string
 		value   Properties
 		wantErr bool
 	}{
-
 		{`{"foo":{"type":"string"}, "bar":{"type":"number"}}`, Properties{
 			{Name: "foo", Schema: &Schema{Type: "string"}},
 			{Name: "bar", Schema: &Schema{Type: "number"}},
@@ -29,27 +95,15 @@ func TestProperties(t *testing.T) {
 	}
 	for i, tt := range tests {
 		tt := tt
-		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
-			a := require.New(t)
-
-			var val Properties
-			err := yaml.Unmarshal([]byte(tt.data), &val)
-			if tt.wantErr {
-				a.Error(err)
-				t.Log("Input:", tt.data)
-				t.Log("Error:", err)
-				return
-			}
-			a.NoError(err)
-
-			data, err := json.Marshal(val)
-			a.NoError(err)
-			a.JSONEq(tt.data, string(data))
-		})
+		t.Run(fmt.Sprintf("Test%d", i+1), testCustomEncodings(create, tt.data, tt.wantErr))
 	}
 }
 
 func TestAdditionalProperties(t *testing.T) {
+	create := func() interface{} {
+		return &AdditionalProperties{}
+	}
+
 	tests := []struct {
 		data    string
 		value   AdditionalProperties
@@ -67,27 +121,15 @@ func TestAdditionalProperties(t *testing.T) {
 	}
 	for i, tt := range tests {
 		tt := tt
-		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
-			a := require.New(t)
-
-			var val AdditionalProperties
-			err := yaml.Unmarshal([]byte(tt.data), &val)
-			if tt.wantErr {
-				a.Error(err)
-				t.Log("Input:", tt.data)
-				t.Log("Error:", err)
-				return
-			}
-			a.NoError(err)
-
-			data, err := json.Marshal(val)
-			a.NoError(err)
-			a.JSONEq(tt.data, string(data))
-		})
+		t.Run(fmt.Sprintf("Test%d", i+1), testCustomEncodings(create, tt.data, tt.wantErr))
 	}
 }
 
 func TestPatternProperties(t *testing.T) {
+	create := func() interface{} {
+		return &PatternProperties{}
+	}
+
 	tests := []struct {
 		data    string
 		value   PatternProperties
@@ -106,22 +148,6 @@ func TestPatternProperties(t *testing.T) {
 	}
 	for i, tt := range tests {
 		tt := tt
-		t.Run(fmt.Sprintf("Test%d", i+1), func(t *testing.T) {
-			a := require.New(t)
-
-			var val PatternProperties
-			err := yaml.Unmarshal([]byte(tt.data), &val)
-			if tt.wantErr {
-				a.Error(err)
-				t.Log("Input:", tt.data)
-				t.Log("Error:", err)
-				return
-			}
-			a.NoError(err)
-
-			data, err := json.Marshal(val)
-			a.NoError(err)
-			a.JSONEq(tt.data, string(data))
-		})
+		t.Run(fmt.Sprintf("Test%d", i+1), testCustomEncodings(create, tt.data, tt.wantErr))
 	}
 }