Repository: ogen-go/ogen
Pull request #155 (2022-02-06 10:23:14)

feat: jsonschema package
Decouples json schema from openapi

cloudflare-pages[bot]: ## Deploying with &nbsp;<a href="https://pages.dev"><img alt="Cloudflare Pages" src="https://user-images.githubusercontent.com/23264/106598434-9e719e00-654f-11eb-9e59-6167043cfa01.png" width="16"></a> &nbsp;Cloudflare Pages

<table><tr><td><strong>Latest commit:</strong> </td><td>
<code>48cab22</code>
</td></tr>
<tr><td><strong>Status:</strong></td><td>&nbsp;✅&nbsp; Deploy successful!</td></tr>
<tr><td><strong>Preview URL:</strong></td><td>
<a href='https://f49de07b.ogen.pages.dev'>https://f49de07b.ogen.pages.dev</a>
</td></tr>
</table>

[View logs](https://dash.cloudflare.com/?to=/:account/pages/view/ogen/f49de07b-3e04-4b67-9553-d6993e5487b0)

codecov[bot]: # [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/155?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) Report
> Merging [#155](https://codecov.io/gh/ogen-go/ogen/pull/155?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) (48cab22) into [main](https://codecov.io/gh/ogen-go/ogen/commit/d3f85ed7b9d3af9752d6c11bd237e60da1ef2587?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) (d3f85ed) will **decrease** coverage by `3.06%`.
> The diff coverage is `30.73%`.

[![Impacted file tree graph](https://codecov.io/gh/ogen-go/ogen/pull/155/graphs/tree.svg?width=650&height=150&src=pr&token=ZH7F81JBY7&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)](https://codecov.io/gh/ogen-go/ogen/pull/155?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)

```diff
@@            Coverage Diff             @@
##             main     #155      +/-   ##
==========================================
- Coverage   73.69%   70.63%   -3.07%     
==========================================
  Files          80       86       +6     
  Lines        5182     5622     +440     
==========================================
+ Hits         3819     3971     +152     
- Misses       1130     1402     +272     
- Partials      233      249      +16     
```


| [Impacted Files](https://codecov.io/gh/ogen-go/ogen/pull/155?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go) | Coverage Δ | |
|---|---|---|
| [internal/jsonschema/parser\_enum.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aW50ZXJuYWwvanNvbnNjaGVtYS9wYXJzZXJfZW51bS5nbw==) | `0.00% <0.00%> (ø)` | |
| [internal/jsonschema/raw\_schema.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aW50ZXJuYWwvanNvbnNjaGVtYS9yYXdfc2NoZW1hLmdv) | `0.00% <0.00%> (ø)` | |
| [internal/jsonschema/schema.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aW50ZXJuYWwvanNvbnNjaGVtYS9zY2hlbWEuZ28=) | `25.00% <25.00%> (ø)` | |
| [internal/jsonschema/parser.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aW50ZXJuYWwvanNvbnNjaGVtYS9wYXJzZXIuZ28=) | `43.68% <43.68%> (ø)` | |
| [internal/jsonschema/parser\_settings.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aW50ZXJuYWwvanNvbnNjaGVtYS9wYXJzZXJfc2V0dGluZ3MuZ28=) | `66.66% <66.66%> (ø)` | |
| [gen/schema\_gen\_sum.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-Z2VuL3NjaGVtYV9nZW5fc3VtLmdv) | `89.39% <0.00%> (-1.85%)` | :arrow_down: |
| [validate/object.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-dmFsaWRhdGUvb2JqZWN0Lmdv) | `100.00% <0.00%> (ø)` | |
| [internal/ir/validation.go](https://codecov.io/gh/ogen-go/ogen/pull/155/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go#diff-aW50ZXJuYWwvaXIvdmFsaWRhdGlvbi5nbw==) | `92.72% <0.00%> (+0.72%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/ogen-go/ogen/pull/155?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/ogen-go/ogen/pull/155?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Last update [d3f85ed...48cab22](https://codecov.io/gh/ogen-go/ogen/pull/155?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=ogen-go).


Patch:
diff --git a/dsl.go b/dsl.go
index 0efa3e53..bbeb3eb6 100644
--- a/dsl.go
+++ b/dsl.go
@@ -6,6 +6,7 @@ import (
 
 	"github.com/ogen-go/ogen/internal/ir"
 	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 // NewSpec returns a new Spec.
@@ -1003,7 +1004,7 @@ func schema(t, f string) *Schema {
 // AsArray returns a new "array" Schema wrapping the receiver.
 func (s *Schema) AsArray() *Schema {
 	return &Schema{
-		Type:  string(oas.Array),
+		Type:  string(jsonschema.Array),
 		Items: s,
 	}
 }
diff --git a/gen/gen_contents.go b/gen/gen_contents.go
index afdbe428..2761eab1 100644
--- a/gen/gen_contents.go
+++ b/gen/gen_contents.go
@@ -3,10 +3,10 @@ package gen
 import (
 	"github.com/go-faster/errors"
 	"github.com/ogen-go/ogen/internal/ir"
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
-func (g *Generator) generateContents(name string, contents map[string]*oas.Schema) (map[ir.ContentType]*ir.Type, error) {
+func (g *Generator) generateContents(name string, contents map[string]*jsonschema.Schema) (map[ir.ContentType]*ir.Type, error) {
 	var (
 		result      = make(map[ir.ContentType]*ir.Type, len(contents))
 		unsupported []string
diff --git a/gen/gen_parameters.go b/gen/gen_parameters.go
index 6d53d729..780031a4 100644
--- a/gen/gen_parameters.go
+++ b/gen/gen_parameters.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/ogen-go/ogen/internal/ir"
 	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 func (g *Generator) generateParameters(opName string, params []*oas.Parameter) ([]*ir.Parameter, error) {
@@ -122,7 +123,7 @@ func isSupportedParamStyle(param *oas.Parameter) error {
 		return &ErrNotImplemented{Name: "spaceDelimited parameter style"}
 
 	case oas.QueryStylePipeDelimited:
-		if param.Schema.Type == oas.Object {
+		if param.Schema.Type == jsonschema.Object {
 			return &ErrNotImplemented{Name: "pipeDelimited style for object parameters"}
 		}
 	}
diff --git a/gen/gen_schema.go b/gen/gen_schema.go
index ca87e237..c36f21fa 100644
--- a/gen/gen_schema.go
+++ b/gen/gen_schema.go
@@ -2,10 +2,10 @@ package gen
 
 import (
 	"github.com/ogen-go/ogen/internal/ir"
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
-func (g *Generator) generateSchema(name string, schema *oas.Schema) (*ir.Type, error) {
+func (g *Generator) generateSchema(name string, schema *jsonschema.Schema) (*ir.Type, error) {
 	gen := &schemaGen{
 		localRefs:  map[string]*ir.Type{},
 		globalRefs: g.refs.types,
diff --git a/gen/schema_gen.go b/gen/schema_gen.go
index 3adaece7..aabfb279 100644
--- a/gen/schema_gen.go
+++ b/gen/schema_gen.go
@@ -8,7 +8,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen/internal/ir"
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 type schemaGen struct {
@@ -37,7 +37,7 @@ func variantFieldName(t *ir.Type) string {
 	return capitalize(result)
 }
 
-func (g *schemaGen) generate(name string, schema *oas.Schema) (_ *ir.Type, err error) {
+func (g *schemaGen) generate(name string, schema *jsonschema.Schema) (_ *ir.Type, err error) {
 	if schema == nil {
 		return nil, &ErrNotImplemented{Name: "empty schema"}
 	}
@@ -93,7 +93,7 @@ func (g *schemaGen) generate(name string, schema *oas.Schema) (_ *ir.Type, err e
 	}
 
 	switch schema.Type {
-	case oas.Object:
+	case jsonschema.Object:
 		kind := ir.KindStruct
 		if schema.AdditionalProperties {
 			kind = ir.KindMap
@@ -135,7 +135,7 @@ func (g *schemaGen) generate(name string, schema *oas.Schema) (_ *ir.Type, err e
 		}
 
 		return s, nil
-	case oas.Array:
+	case jsonschema.Array:
 		array := &ir.Type{
 			Kind:        ir.KindArray,
 			Schema:      schema,
@@ -156,24 +156,24 @@ func (g *schemaGen) generate(name string, schema *oas.Schema) (_ *ir.Type, err e
 
 		return ret, nil
 
-	case oas.String, oas.Integer, oas.Number, oas.Boolean:
+	case jsonschema.String, jsonschema.Integer, jsonschema.Number, jsonschema.Boolean:
 		t, err := g.primitive(name, schema)
 		if err != nil {
 			return nil, errors.Wrap(err, "primitive")
 		}
 
 		switch schema.Type {
-		case oas.String:
+		case jsonschema.String:
 			if err := t.Validators.SetString(schema); err != nil {
 				return nil, errors.Wrap(err, "string validator")
 			}
 
-		case oas.Integer, oas.Number:
+		case jsonschema.Integer, jsonschema.Number:
 			t.Validators.SetInt(schema)
 		}
 
 		return side(t), nil
-	case oas.Empty:
+	case jsonschema.Empty:
 		return side(ir.Any()), nil
 	default:
 		panic("unreachable")
diff --git a/gen/schema_gen_primitive.go b/gen/schema_gen_primitive.go
index 1ea12408..b4d77706 100644
--- a/gen/schema_gen_primitive.go
+++ b/gen/schema_gen_primitive.go
@@ -6,10 +6,10 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen/internal/ir"
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
-func (g *schemaGen) primitive(name string, schema *oas.Schema) (*ir.Type, error) {
+func (g *schemaGen) primitive(name string, schema *jsonschema.Schema) (*ir.Type, error) {
 	t, err := parseSimple(schema)
 	if err != nil {
 		return nil, err
@@ -70,43 +70,43 @@ func (g *schemaGen) primitive(name string, schema *oas.Schema) (*ir.Type, error)
 	return t, nil
 }
 
-func parseSimple(schema *oas.Schema) (*ir.Type, error) {
-	mapping := map[oas.SchemaType]map[oas.Format]ir.PrimitiveType{
-		oas.Integer: {
-			oas.FormatInt32: ir.Int32,
-			oas.FormatInt64: ir.Int64,
-			oas.FormatNone:  ir.Int,
+func parseSimple(schema *jsonschema.Schema) (*ir.Type, error) {
+	mapping := map[jsonschema.SchemaType]map[jsonschema.Format]ir.PrimitiveType{
+		jsonschema.Integer: {
+			jsonschema.FormatInt32: ir.Int32,
+			jsonschema.FormatInt64: ir.Int64,
+			jsonschema.FormatNone:  ir.Int,
 		},
-		oas.Number: {
-			oas.FormatFloat:  ir.Float32,
-			oas.FormatDouble: ir.Float64,
-			oas.FormatNone:   ir.Float64,
-			oas.FormatInt32:  ir.Int32,
-			oas.FormatInt64:  ir.Int64,
+		jsonschema.Number: {
+			jsonschema.FormatFloat:  ir.Float32,
+			jsonschema.FormatDouble: ir.Float64,
+			jsonschema.FormatNone:   ir.Float64,
+			jsonschema.FormatInt32:  ir.Int32,
+			jsonschema.FormatInt64:  ir.Int64,
 		},
-		oas.String: {
-			oas.FormatByte:     ir.ByteSlice,
-			oas.FormatDateTime: ir.Time,
-			oas.FormatDate:     ir.Time,
-			oas.FormatTime:     ir.Time,
-			oas.FormatDuration: ir.Duration,
-			oas.FormatUUID:     ir.UUID,
-			oas.FormatIP:       ir.IP,
-			oas.FormatIPv4:     ir.IP,
-			oas.FormatIPv6:     ir.IP,
-			oas.FormatURI:      ir.URL,
-			oas.FormatPassword: ir.String,
-			oas.FormatNone:     ir.String,
+		jsonschema.String: {
+			jsonschema.FormatByte:     ir.ByteSlice,
+			jsonschema.FormatDateTime: ir.Time,
+			jsonschema.FormatDate:     ir.Time,
+			jsonschema.FormatTime:     ir.Time,
+			jsonschema.FormatDuration: ir.Duration,
+			jsonschema.FormatUUID:     ir.UUID,
+			jsonschema.FormatIP:       ir.IP,
+			jsonschema.FormatIPv4:     ir.IP,
+			jsonschema.FormatIPv6:     ir.IP,
+			jsonschema.FormatURI:      ir.URL,
+			jsonschema.FormatPassword: ir.String,
+			jsonschema.FormatNone:     ir.String,
 		},
-		oas.Boolean: {
-			oas.FormatNone: ir.Bool,
+		jsonschema.Boolean: {
+			jsonschema.FormatNone: ir.Bool,
 		},
 	}
 
 	t, found := mapping[schema.Type][schema.Format]
 	if !found {
 		// Return string type for unknown string formats.
-		if schema.Type == oas.String {
+		if schema.Type == jsonschema.String {
 			return ir.Primitive(ir.String, schema), nil
 		}
 		return nil, errors.Errorf("unexpected %q format: %q", schema.Type, schema.Format)
diff --git a/gen/schema_gen_sum.go b/gen/schema_gen_sum.go
index ffa69299..df0dc0a1 100644
--- a/gen/schema_gen_sum.go
+++ b/gen/schema_gen_sum.go
@@ -9,7 +9,7 @@ import (
 	"github.com/go-faster/errors"
 
 	"github.com/ogen-go/ogen/internal/ir"
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 func canUseTypeDiscriminator(sum []*ir.Type) bool {
@@ -33,7 +33,7 @@ func canUseTypeDiscriminator(sum []*ir.Type) bool {
 	return true
 }
 
-func (g *schemaGen) collectSumVariants(name string, schemas []*oas.Schema) (sum []*ir.Type, _ error) {
+func (g *schemaGen) collectSumVariants(name string, schemas []*jsonschema.Schema) (sum []*ir.Type, _ error) {
 	names := map[string]struct{}{}
 	for i, s := range schemas {
 		t, err := g.generate(fmt.Sprintf("%s%d", name, i), s)
@@ -52,7 +52,7 @@ func (g *schemaGen) collectSumVariants(name string, schemas []*oas.Schema) (sum
 	return sum, nil
 }
 
-func (g *schemaGen) anyOf(name string, schema *oas.Schema) (*ir.Type, error) {
+func (g *schemaGen) anyOf(name string, schema *jsonschema.Schema) (*ir.Type, error) {
 	sum := &ir.Type{
 		Name:   name,
 		Kind:   ir.KindSum,
@@ -95,7 +95,7 @@ func (g *schemaGen) anyOf(name string, schema *oas.Schema) (*ir.Type, error) {
 	return nil, &ErrNotImplemented{"complex anyOf"}
 }
 
-func (g *schemaGen) oneOf(name string, schema *oas.Schema) (*ir.Type, error) {
+func (g *schemaGen) oneOf(name string, schema *jsonschema.Schema) (*ir.Type, error) {
 	sum := &ir.Type{
 		Name:   name,
 		Kind:   ir.KindSum,
@@ -152,10 +152,10 @@ func (g *schemaGen) oneOf(name string, schema *oas.Schema) (*ir.Type, error) {
 	}
 
 	// 3rd case: distinguish by unique fields.
-	var (
-		// Determine unique fields for each SumOf variant.
-		uniq = map[string]map[string]struct{}{}
-	)
+
+	// Determine unique fields for each SumOf variant.
+	uniq := map[string]map[string]struct{}{}
+
 	for _, s := range sum.SumOf {
 		uniq[s.Name] = map[string]struct{}{}
 		if !s.Is(ir.KindMap, ir.KindStruct) {
diff --git a/gen/utils.go b/gen/utils.go
index 927fcc36..e0b9a0a3 100644
--- a/gen/utils.go
+++ b/gen/utils.go
@@ -1,19 +1,19 @@
 package gen
 
 import (
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
-func isBinary(s *oas.Schema) bool {
+func isBinary(s *jsonschema.Schema) bool {
 	if s == nil {
 		return false
 	}
 
 	switch s.Type {
-	case "", oas.String:
+	case "", jsonschema.String:
 	default:
 		return false
 	}
 
-	return s.Format == oas.FormatBinary
+	return s.Format == jsonschema.FormatBinary
 }
diff --git a/internal/ir/constructors.go b/internal/ir/constructors.go
index bd32a0ad..13fc027c 100644
--- a/internal/ir/constructors.go
+++ b/internal/ir/constructors.go
@@ -1,8 +1,8 @@
 package ir
 
-import "github.com/ogen-go/ogen/internal/oas"
+import "github.com/ogen-go/ogen/jsonschema"
 
-func Primitive(typ PrimitiveType, schema *oas.Schema) *Type {
+func Primitive(typ PrimitiveType, schema *jsonschema.Schema) *Type {
 	return &Type{
 		Kind:      KindPrimitive,
 		Primitive: typ,
@@ -10,7 +10,7 @@ func Primitive(typ PrimitiveType, schema *oas.Schema) *Type {
 	}
 }
 
-func Array(item *Type, sem NilSemantic, schema *oas.Schema) *Type {
+func Array(item *Type, sem NilSemantic, schema *jsonschema.Schema) *Type {
 	return &Type{
 		Kind:        KindArray,
 		Item:        item,
diff --git a/internal/ir/json.go b/internal/ir/json.go
index 1de2e495..295066e2 100644
--- a/internal/ir/json.go
+++ b/internal/ir/json.go
@@ -1,8 +1,6 @@
 package ir
 
-import (
-	"github.com/ogen-go/ogen/internal/oas"
-)
+import "github.com/ogen-go/ogen/jsonschema"
 
 // JSON returns json encoding/decoding rules for t.
 func (t *Type) JSON() JSON {
@@ -97,19 +95,19 @@ func (j JSON) Format() string {
 		return ""
 	}
 	switch j.t.Schema.Format {
-	case oas.FormatUUID:
+	case jsonschema.FormatUUID:
 		return "UUID"
-	case oas.FormatDate:
+	case jsonschema.FormatDate:
 		return "Date"
-	case oas.FormatTime:
+	case jsonschema.FormatTime:
 		return "Time"
-	case oas.FormatDateTime:
+	case jsonschema.FormatDateTime:
 		return "DateTime"
-	case oas.FormatDuration:
+	case jsonschema.FormatDuration:
 		return "Duration"
-	case oas.FormatIP, oas.FormatIPv4, oas.FormatIPv6:
+	case jsonschema.FormatIP, jsonschema.FormatIPv4, jsonschema.FormatIPv6:
 		return "IP"
-	case oas.FormatURI:
+	case jsonschema.FormatURI:
 		return "URI"
 	default:
 		return ""
diff --git a/internal/ir/type.go b/internal/ir/type.go
index fe1209bf..600ffd46 100644
--- a/internal/ir/type.go
+++ b/internal/ir/type.go
@@ -6,7 +6,7 @@ import (
 	"strconv"
 	"strings"
 
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 type Kind string
@@ -94,7 +94,7 @@ type Type struct {
 	Implements       map[*Type]struct{}  // only for struct, alias, enum
 	Implementations  map[*Type]struct{}  // only for interface
 	InterfaceMethods map[string]struct{} // only for interface
-	Schema           *oas.Schema         // for all kinds except pointer, interface. Can be nil.
+	Schema           *jsonschema.Schema  // for all kinds except pointer, interface. Can be nil.
 	NilSemantic      NilSemantic         // only for pointer
 	GenericOf        *Type               // only for generic
 	GenericVariant   GenericVariant      // only for generic
@@ -168,7 +168,7 @@ type Field struct {
 	Name string
 	Type *Type
 	Tag  Tag
-	Spec *oas.Property
+	Spec *jsonschema.Property
 }
 
 func (f Field) GoDoc() []string {
diff --git a/internal/ir/validation.go b/internal/ir/validation.go
index 4ce65d60..f5c19834 100644
--- a/internal/ir/validation.go
+++ b/internal/ir/validation.go
@@ -5,7 +5,7 @@ import (
 
 	"github.com/go-faster/errors"
 
-	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/ogen-go/ogen/validate"
 )
 
@@ -16,7 +16,7 @@ type Validators struct {
 	Object validate.Object
 }
 
-func (v *Validators) SetString(schema *oas.Schema) (err error) {
+func (v *Validators) SetString(schema *jsonschema.Schema) (err error) {
 	if schema.Pattern != "" {
 		v.String.Regex, err = regexp.Compile(schema.Pattern)
 		if err != nil {
@@ -29,16 +29,16 @@ func (v *Validators) SetString(schema *oas.Schema) (err error) {
 	if schema.MinLength != nil {
 		v.String.SetMinLength(int(*schema.MinLength))
 	}
-	if schema.Format == oas.FormatEmail {
+	if schema.Format == jsonschema.FormatEmail {
 		v.String.Email = true
 	}
-	if schema.Format == oas.FormatHostname {
+	if schema.Format == jsonschema.FormatHostname {
 		v.String.Hostname = true
 	}
 	return nil
 }
 
-func (v *Validators) SetInt(schema *oas.Schema) {
+func (v *Validators) SetInt(schema *jsonschema.Schema) {
 	if schema.MultipleOf != nil {
 		v.Int.SetMultipleOf(*schema.MultipleOf)
 	}
@@ -52,7 +52,7 @@ func (v *Validators) SetInt(schema *oas.Schema) {
 	v.Int.MinExclusive = schema.ExclusiveMinimum
 }
 
-func (v *Validators) SetArray(schema *oas.Schema) {
+func (v *Validators) SetArray(schema *jsonschema.Schema) {
 	if schema.MaxItems != nil {
 		v.Array.SetMaxLength(int(*schema.MaxItems))
 	}
@@ -61,7 +61,7 @@ func (v *Validators) SetArray(schema *oas.Schema) {
 	}
 }
 
-func (v *Validators) SetObject(schema *oas.Schema) {
+func (v *Validators) SetObject(schema *jsonschema.Schema) {
 	if schema.MaxProperties != nil {
 		v.Object.SetMaxProperties(int(*schema.MaxProperties))
 	}
diff --git a/internal/oas/operation.go b/internal/oas/operation.go
index b15b025f..cbf443fd 100644
--- a/internal/oas/operation.go
+++ b/internal/oas/operation.go
@@ -1,5 +1,7 @@
 package oas
 
+import "github.com/ogen-go/ogen/jsonschema"
+
 // Operation is an OpenAPI Operation.
 type Operation struct {
 	OperationID string // optional
@@ -40,12 +42,12 @@ type PathPart struct {
 
 // RequestBody of an OpenAPI Operation.
 type RequestBody struct {
-	Contents map[string]*Schema
+	Contents map[string]*jsonschema.Schema
 	Required bool
 }
 
 // Response is an OpenAPI Response definition.
 type Response struct {
 	Ref      string
-	Contents map[string]*Schema
+	Contents map[string]*jsonschema.Schema
 }
diff --git a/internal/oas/parameter.go b/internal/oas/parameter.go
index dafb4d58..646fd84a 100644
--- a/internal/oas/parameter.go
+++ b/internal/oas/parameter.go
@@ -1,5 +1,7 @@
 package oas
 
+import "github.com/ogen-go/ogen/jsonschema"
+
 // ParameterLocation defines where OpenAPI parameter is located.
 type ParameterLocation string
 
@@ -22,7 +24,7 @@ func (l ParameterLocation) Cookie() bool { return l == LocationCookie }
 type Parameter struct {
 	Name        string
 	Description string
-	Schema      *Schema
+	Schema      *jsonschema.Schema
 	In          ParameterLocation
 	Style       ParameterStyle
 	Explode     bool
diff --git a/internal/oas/parser/parse_parameter.go b/internal/oas/parser/parse_parameter.go
index 02816a80..ab867f96 100644
--- a/internal/oas/parser/parse_parameter.go
+++ b/internal/oas/parser/parse_parameter.go
@@ -68,7 +68,7 @@ func (p *parser) parseParameter(param *ogen.Parameter) (*oas.Parameter, error) {
 		return nil, errors.New("path parameters must be required")
 	}
 
-	schema, err := p.parseSchema(&param.Schema)
+	schema, err := p.schemaParser.Parse(param.Schema.ToJSONSchema())
 	if err != nil {
 		return nil, errors.Wrap(err, "schema")
 	}
@@ -77,7 +77,6 @@ func (p *parser) parseParameter(param *ogen.Parameter) (*oas.Parameter, error) {
 	if err != nil {
 		return nil, errors.Wrap(err, "style")
 	}
-
 	return &oas.Parameter{
 		Name:        param.Name,
 		Description: param.Description,
diff --git a/internal/oas/parser/parse_request_body.go b/internal/oas/parser/parse_request_body.go
index dec20549..73c4aa86 100644
--- a/internal/oas/parser/parse_request_body.go
+++ b/internal/oas/parser/parse_request_body.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 func (p *parser) parseRequestBody(body *ogen.RequestBody) (*oas.RequestBody, error) {
@@ -18,12 +19,12 @@ func (p *parser) parseRequestBody(body *ogen.RequestBody) (*oas.RequestBody, err
 	}
 
 	result := &oas.RequestBody{
-		Contents: make(map[string]*oas.Schema, len(body.Content)),
+		Contents: make(map[string]*jsonschema.Schema, len(body.Content)),
 		Required: body.Required,
 	}
 
 	for contentType, media := range body.Content {
-		schema, err := p.parseSchema(media.Schema)
+		schema, err := p.schemaParser.Parse(media.Schema.ToJSONSchema())
 		if err != nil {
 			return nil, errors.Wrapf(err, "content: %s: parse schema", contentType)
 		}
diff --git a/internal/oas/parser/parse_response.go b/internal/oas/parser/parse_response.go
index 7634a430..63328692 100644
--- a/internal/oas/parser/parse_response.go
+++ b/internal/oas/parser/parse_response.go
@@ -8,6 +8,7 @@ import (
 
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 func (p *parser) parseResponses(responses ogen.Responses) (map[string]*oas.Response, error) {
@@ -43,10 +44,10 @@ func (p *parser) parseResponse(resp *ogen.Response) (*oas.Response, error) {
 	}
 
 	response := &oas.Response{
-		Contents: make(map[string]*oas.Schema, len(resp.Content)),
+		Contents: make(map[string]*jsonschema.Schema, len(resp.Content)),
 	}
 	for contentType, media := range resp.Content {
-		schema, err := p.parseSchema(media.Schema)
+		schema, err := p.schemaParser.Parse(media.Schema.ToJSONSchema())
 		if err != nil {
 			return nil, errors.Wrapf(err, "content: %s: schema", contentType)
 		}
diff --git a/internal/oas/parser/parse_schema.go b/internal/oas/parser/parse_schema.go
deleted file mode 100644
index 41223269..00000000
--- a/internal/oas/parser/parse_schema.go
+++ /dev/null
@@ -1,39 +0,0 @@
-package parser
-
-import (
-	"fmt"
-
-	"github.com/go-faster/errors"
-
-	"github.com/ogen-go/ogen"
-	"github.com/ogen-go/ogen/internal/oas"
-)
-
-func (p *parser) parseSchema(schema *ogen.Schema) (*oas.Schema, error) {
-	if schema == nil {
-		return nil, nil
-	}
-
-	parser := &schemaParser{
-		components: p.spec.Components.Schemas,
-		globalRefs: p.refs.schemas,
-		localRefs:  make(map[string]*oas.Schema),
-		inferTypes: p.inferTypes,
-	}
-
-	s, err := parser.Parse(schema)
-	if err != nil {
-		return nil, errors.Wrap(err, "generate")
-	}
-
-	// Merge references.
-	for ref, schema := range parser.localRefs {
-		if _, found := p.refs.schemas[ref]; found {
-			panic(fmt.Sprintf("schema reference conflict: %s", ref))
-		}
-
-		p.refs.schemas[ref] = schema
-	}
-
-	return s, nil
-}
diff --git a/internal/oas/parser/parser.go b/internal/oas/parser/parser.go
index 9ac230a9..8f62d67b 100644
--- a/internal/oas/parser/parser.go
+++ b/internal/oas/parser/parser.go
@@ -7,6 +7,7 @@ import (
 
 	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 type parser struct {
@@ -16,13 +17,13 @@ type parser struct {
 	operations []*oas.Operation
 	// refs contains lazy-initialized referenced components.
 	refs struct {
-		schemas       map[string]*oas.Schema
 		requestBodies map[string]*oas.RequestBody
 		responses     map[string]*oas.Response
 		parameters    map[string]*oas.Parameter
 		examples      map[string]*oas.Example
 	}
-	inferTypes bool
+
+	schemaParser *jsonschema.Parser
 }
 
 func Parse(spec *ogen.Spec, inferTypes bool) ([]*oas.Operation, error) {
@@ -31,19 +32,20 @@ func Parse(spec *ogen.Spec, inferTypes bool) ([]*oas.Operation, error) {
 		spec:       spec,
 		operations: nil,
 		refs: struct {
-			schemas       map[string]*oas.Schema
 			requestBodies map[string]*oas.RequestBody
 			responses     map[string]*oas.Response
 			parameters    map[string]*oas.Parameter
 			examples      map[string]*oas.Example
 		}{
-			schemas:       map[string]*oas.Schema{},
 			requestBodies: map[string]*oas.RequestBody{},
 			responses:     map[string]*oas.Response{},
 			parameters:    map[string]*oas.Parameter{},
 			examples:      map[string]*oas.Example{},
 		},
-		inferTypes: inferTypes,
+		schemaParser: jsonschema.NewParser(jsonschema.Settings{
+			Resolver:   componentsResolver(spec.Components.Schemas),
+			InferTypes: inferTypes,
+		}),
 	}
 
 	err := p.parse()
diff --git a/internal/oas/parser/ref.go b/internal/oas/parser/ref.go
index 5f655692..7bfaef2d 100644
--- a/internal/oas/parser/ref.go
+++ b/internal/oas/parser/ref.go
@@ -5,9 +5,28 @@ import (
 
 	"github.com/go-faster/errors"
 
+	"github.com/ogen-go/ogen"
 	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
+type componentsResolver map[string]*ogen.Schema
+
+func (c componentsResolver) ResolveReference(ref string) (*jsonschema.RawSchema, error) {
+	const prefix = "#/components/schemas/"
+	if !strings.HasPrefix(ref, prefix) {
+		return nil, errors.Errorf("invalid schema reference %q", ref)
+	}
+
+	name := strings.TrimPrefix(ref, prefix)
+	s, ok := c[name]
+	if !ok {
+		return nil, errors.New("schema not found")
+	}
+
+	return s.ToJSONSchema(), nil
+}
+
 func (p *parser) resolveRequestBody(ref string) (*oas.RequestBody, error) {
 	const prefix = "#/components/requestBodies/"
 	if !strings.HasPrefix(ref, prefix) {
diff --git a/internal/oas/format.go b/jsonschema/format.go
similarity index 97%
rename from internal/oas/format.go
rename to jsonschema/format.go
index c69c20a9..62199a0c 100644
--- a/internal/oas/format.go
+++ b/jsonschema/format.go
@@ -1,4 +1,4 @@
-package oas
+package jsonschema
 
 // Format of Schema.
 type Format string
diff --git a/internal/oas/parser/schema_parser.go b/jsonschema/parser.go
similarity index 72%
rename from internal/oas/parser/schema_parser.go
rename to jsonschema/parser.go
index 742bf9b6..57d1007b 100644
--- a/internal/oas/parser/schema_parser.go
+++ b/jsonschema/parser.go
@@ -1,47 +1,37 @@
-package parser
+package jsonschema
 
 import (
-	"strings"
-
 	"github.com/go-faster/errors"
-
-	"github.com/ogen-go/ogen"
-	"github.com/ogen-go/ogen/internal/oas"
 )
 
-// schemaParser is used to parse OpenAPI schemas.
-type schemaParser struct {
-	// Schema components from OpenAPI document (readonly).
-	components map[string]*ogen.Schema
-
-	// Parsed schema components from *parser (readonly).
-	// Used as cache from previous parsing ops.
-	globalRefs map[string]*oas.Schema
-
-	// Parsed schema components current schema refers to.
-	localRefs map[string]*oas.Schema
+// ReferenceResolver resolves JSON schema references.
+type ReferenceResolver interface {
+	ResolveReference(ref string) (*RawSchema, error)
+}
 
-	// Enables type inference.
-	//
-	// For example:
-	//
-	//	{
-	//		"items": {
-	//			"type": "string"
-	//		}
-	//	}
-	//
-	// In that case schemaParser will handle that schema as "array" schema, because it has "items" field.
+// Parser parses JSON schemas.
+type Parser struct {
+	resolver   ReferenceResolver
+	refcache   map[string]*Schema
 	inferTypes bool
 }
 
-func (p *schemaParser) Parse(schema *ogen.Schema) (*oas.Schema, error) {
-	return p.parse(schema, func(s *oas.Schema) *oas.Schema {
+func NewParser(s Settings) *Parser {
+	s.setDefaults()
+	return &Parser{
+		resolver:   s.Resolver,
+		refcache:   map[string]*Schema{},
+		inferTypes: s.InferTypes,
+	}
+}
+
+func (p *Parser) Parse(schema *RawSchema) (*Schema, error) {
+	return p.parse(schema, func(s *Schema) *Schema {
 		return p.extendInfo(schema, s)
 	})
 }
 
-func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Schema) (*oas.Schema, error) {
+func (p *Parser) parse(schema *RawSchema, hook func(*Schema) *Schema) (*Schema, error) {
 	if schema == nil {
 		return nil, nil
 	}
@@ -67,14 +57,14 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return nil, errors.Wrap(err, "validate format")
 		}
 
-		values, err := parseEnumValues(oas.SchemaType(schema.Type), schema.Enum)
+		values, err := parseEnumValues(SchemaType(schema.Type), schema.Enum)
 		if err != nil {
 			return nil, errors.Wrap(err, "parse enum")
 		}
 
-		return hook(&oas.Schema{
-			Type:   oas.SchemaType(schema.Type),
-			Format: oas.Format(schema.Format),
+		return hook(&Schema{
+			Type:   SchemaType(schema.Type),
+			Format: Format(schema.Format),
 			Enum:   values,
 		}), nil
 	case len(schema.OneOf) > 0:
@@ -83,14 +73,14 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return nil, errors.Wrapf(err, "oneOf")
 		}
 
-		return hook(&oas.Schema{OneOf: schemas}), nil
+		return hook(&Schema{OneOf: schemas}), nil
 	case len(schema.AnyOf) > 0:
 		schemas, err := p.parseMany(schema.AnyOf)
 		if err != nil {
 			return nil, errors.Wrapf(err, "anyOf")
 		}
 
-		return hook(&oas.Schema{
+		return hook(&Schema{
 			AnyOf: schemas,
 			// Object validators
 			MaxProperties: schema.MaxProperties,
@@ -116,7 +106,7 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return nil, errors.Wrapf(err, "allOf")
 		}
 
-		return hook(&oas.Schema{AllOf: schemas}), nil
+		return hook(&Schema{AllOf: schemas}), nil
 	}
 
 	// Try to infer schema type from properties.
@@ -161,8 +151,8 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return false
 		}
 
-		s := hook(&oas.Schema{
-			Type:          oas.Object,
+		s := hook(&Schema{
+			Type:          Object,
 			MaxProperties: schema.MaxProperties,
 			MinProperties: schema.MinProperties,
 		})
@@ -184,7 +174,7 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 				return nil, errors.Wrapf(err, "%s", propSpec.Name)
 			}
 
-			s.Properties = append(s.Properties, oas.Property{
+			s.Properties = append(s.Properties, Property{
 				Name:        propSpec.Name,
 				Description: propSpec.Schema.Description,
 				Schema:      prop,
@@ -194,8 +184,8 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 		return s, nil
 
 	case "array":
-		array := hook(&oas.Schema{
-			Type:        oas.Array,
+		array := hook(&Schema{
+			Type:        Array,
 			MinItems:    schema.MinItems,
 			MaxItems:    schema.MaxItems,
 			UniqueItems: schema.UniqueItems,
@@ -221,9 +211,9 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return nil, errors.Wrap(err, "validate format")
 		}
 
-		return hook(&oas.Schema{
-			Type:             oas.SchemaType(schema.Type),
-			Format:           oas.Format(schema.Format),
+		return hook(&Schema{
+			Type:             SchemaType(schema.Type),
+			Format:           Format(schema.Format),
 			Minimum:          schema.Minimum,
 			Maximum:          schema.Maximum,
 			ExclusiveMinimum: schema.ExclusiveMinimum,
@@ -236,9 +226,9 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return nil, errors.Wrap(err, "validate format")
 		}
 
-		return hook(&oas.Schema{
-			Type:   oas.Boolean,
-			Format: oas.Format(schema.Format),
+		return hook(&Schema{
+			Type:   Boolean,
+			Format: Format(schema.Format),
 		}), nil
 
 	case "string":
@@ -246,24 +236,24 @@ func (p *schemaParser) parse(schema *ogen.Schema, hook func(*oas.Schema) *oas.Sc
 			return nil, errors.Wrap(err, "validate format")
 		}
 
-		return hook(&oas.Schema{
-			Type:      oas.String,
-			Format:    oas.Format(schema.Format),
+		return hook(&Schema{
+			Type:      String,
+			Format:    Format(schema.Format),
 			MaxLength: schema.MaxLength,
 			MinLength: schema.MinLength,
 			Pattern:   schema.Pattern,
 		}), nil
 
 	case "":
-		return hook(&oas.Schema{}), nil
+		return hook(&Schema{}), nil
 
 	default:
 		return nil, errors.Errorf("unexpected schema type: %q", schema.Type)
 	}
 }
 
-func (p *schemaParser) parseMany(schemas []*ogen.Schema) ([]*oas.Schema, error) {
-	result := make([]*oas.Schema, 0, len(schemas))
+func (p *Parser) parseMany(schemas []*RawSchema) ([]*Schema, error) {
+	result := make([]*Schema, 0, len(schemas))
 	for i, schema := range schemas {
 		s, err := p.Parse(schema)
 		if err != nil {
@@ -276,34 +266,24 @@ func (p *schemaParser) parseMany(schemas []*ogen.Schema) ([]*oas.Schema, error)
 	return result, nil
 }
 
-func (p *schemaParser) resolve(ref string) (*oas.Schema, error) {
-	const prefix = "#/components/schemas/"
-	if !strings.HasPrefix(ref, prefix) {
-		return nil, errors.Errorf("invalid schema reference %q", ref)
-	}
-
-	if s, ok := p.globalRefs[ref]; ok {
-		return s, nil
-	}
-
-	if s, ok := p.localRefs[ref]; ok {
+func (p *Parser) resolve(ref string) (*Schema, error) {
+	if s, ok := p.refcache[ref]; ok {
 		return s, nil
 	}
 
-	name := strings.TrimPrefix(ref, prefix)
-	component, found := p.components[name]
-	if !found {
-		return nil, errors.Errorf("component by reference %q not found", ref)
+	raw, err := p.resolver.ResolveReference(ref)
+	if err != nil {
+		return nil, errors.Wrapf(err, "resolve reference %q", ref)
 	}
 
-	return p.parse(component, func(s *oas.Schema) *oas.Schema {
+	return p.parse(raw, func(s *Schema) *Schema {
 		s.Ref = ref
-		p.localRefs[ref] = s
-		return p.extendInfo(component, s)
+		p.refcache[ref] = s
+		return p.extendInfo(raw, s)
 	})
 }
 
-func (p *schemaParser) extendInfo(schema *ogen.Schema, s *oas.Schema) *oas.Schema {
+func (p *Parser) extendInfo(schema *RawSchema, s *Schema) *Schema {
 	s.Description = schema.Description
 	s.AddExample(schema.Example)
 
@@ -335,7 +315,7 @@ func (p *schemaParser) extendInfo(schema *ogen.Schema, s *oas.Schema) *oas.Schem
 		}
 	}
 	if d := schema.Discriminator; d != nil {
-		s.Discriminator = &oas.Discriminator{
+		s.Discriminator = &Discriminator{
 			PropertyName: d.PropertyName,
 			Mapping:      d.Mapping,
 		}
diff --git a/internal/oas/parser/schema_parser_enum.go b/jsonschema/parser_enum.go
similarity index 84%
rename from internal/oas/parser/schema_parser_enum.go
rename to jsonschema/parser_enum.go
index 9077cc2e..9686a932 100644
--- a/internal/oas/parser/schema_parser_enum.go
+++ b/jsonschema/parser_enum.go
@@ -1,12 +1,10 @@
-package parser
+package jsonschema
 
 import (
 	"encoding/json"
 
 	"github.com/go-faster/errors"
 	"github.com/go-faster/jx"
-
-	"github.com/ogen-go/ogen/internal/oas"
 )
 
 func inferEnumType(v json.RawMessage) (string, error) {
@@ -25,7 +23,7 @@ func inferEnumType(v json.RawMessage) (string, error) {
 	}
 }
 
-func parseEnumValues(typ oas.SchemaType, rawValues []json.RawMessage) ([]interface{}, error) {
+func parseEnumValues(typ SchemaType, rawValues []json.RawMessage) ([]interface{}, error) {
 	var (
 		values []interface{}
 		unique = map[interface{}]struct{}{}
@@ -46,7 +44,7 @@ func parseEnumValues(typ oas.SchemaType, rawValues []json.RawMessage) ([]interfa
 	return values, nil
 }
 
-func parseJSONValue(typ oas.SchemaType, v json.RawMessage) (interface{}, error) {
+func parseJSONValue(typ SchemaType, v json.RawMessage) (interface{}, error) {
 	var (
 		d    = jx.DecodeBytes(v)
 		next = d.Next()
@@ -55,12 +53,12 @@ func parseJSONValue(typ oas.SchemaType, v json.RawMessage) (interface{}, error)
 		return nil, nil
 	}
 	switch typ {
-	case oas.String:
+	case String:
 		if next != jx.String {
 			return nil, errors.Errorf("expected type %q, got %q", typ, next)
 		}
 		return d.Str()
-	case oas.Integer:
+	case Integer:
 		if next != jx.Number {
 			return nil, errors.Errorf("expected type %q, got %q", typ, next)
 		}
@@ -72,7 +70,7 @@ func parseJSONValue(typ oas.SchemaType, v json.RawMessage) (interface{}, error)
 			return nil, errors.New("expected integer, got float")
 		}
 		return n.Int64()
-	case oas.Number:
+	case Number:
 		if next != jx.Number {
 			return nil, errors.Errorf("expected type %q, got %q", typ, next)
 		}
@@ -81,7 +79,7 @@ func parseJSONValue(typ oas.SchemaType, v json.RawMessage) (interface{}, error)
 			return nil, err
 		}
 		return n.Float64()
-	case oas.Boolean:
+	case Boolean:
 		if next != jx.Bool {
 			return nil, errors.Errorf("expected type %q, got %q", typ, next)
 		}
diff --git a/jsonschema/parser_settings.go b/jsonschema/parser_settings.go
new file mode 100644
index 00000000..73f362be
--- /dev/null
+++ b/jsonschema/parser_settings.go
@@ -0,0 +1,32 @@
+package jsonschema
+
+import "errors"
+
+type Settings struct {
+	Resolver ReferenceResolver
+
+	// Enables type inference.
+	//
+	// For example:
+	//
+	//	{
+	//		"items": {
+	//			"type": "string"
+	//		}
+	//	}
+	//
+	// In that case schemaParser will handle that schema as "array" schema, because it has "items" field.
+	InferTypes bool
+}
+
+type nopResolver struct{}
+
+func (nopResolver) ResolveReference(ref string) (*RawSchema, error) {
+	return nil, errors.New("reference resolver is not provided")
+}
+
+func (s *Settings) setDefaults() {
+	if s.Resolver == nil {
+		s.Resolver = nopResolver{}
+	}
+}
diff --git a/jsonschema/raw_schema.go b/jsonschema/raw_schema.go
new file mode 100644
index 00000000..54466b3f
--- /dev/null
+++ b/jsonschema/raw_schema.go
@@ -0,0 +1,124 @@
+package jsonschema
+
+import (
+	"encoding/json"
+
+	"github.com/go-faster/errors"
+	"github.com/go-faster/jx"
+)
+
+type RawSchema struct {
+	Ref                  string                `json:"$ref,omitempty"`
+	Description          string                `json:"description,omitempty"`
+	Type                 string                `json:"type,omitempty"`
+	Format               string                `json:"format,omitempty"`
+	Properties           RawProperties         `json:"properties,omitempty"`
+	AdditionalProperties *AdditionalProperties `json:"additionalProperties,omitempty"`
+	Required             []string              `json:"required,omitempty"`
+	Items                *RawSchema            `json:"items,omitempty"`
+	Nullable             bool                  `json:"nullable,omitempty"`
+	AllOf                []*RawSchema          `json:"allOf,omitempty"`
+	OneOf                []*RawSchema          `json:"oneOf,omitempty"`
+	AnyOf                []*RawSchema          `json:"anyOf,omitempty"`
+	Discriminator        *Discriminator        `json:"discriminator,omitempty"`
+	Enum                 []json.RawMessage     `json:"enum,omitempty"`
+	MultipleOf           *int                  `json:"multipleOf,omitempty"`
+	Maximum              *int64                `json:"maximum,omitempty"`
+	ExclusiveMaximum     bool                  `json:"exclusiveMaximum,omitempty"`
+	Minimum              *int64                `json:"minimum,omitempty"`
+	ExclusiveMinimum     bool                  `json:"exclusiveMinimum,omitempty"`
+	MaxLength            *uint64               `json:"maxLength,omitempty"`
+	MinLength            *int64                `json:"minLength,omitempty"`
+	Pattern              string                `json:"pattern,omitempty"`
+	MaxItems             *uint64               `json:"maxItems,omitempty"`
+	MinItems             *uint64               `json:"minItems,omitempty"`
+	UniqueItems          bool                  `json:"uniqueItems,omitempty"`
+	MaxProperties        *uint64               `json:"maxProperties,omitempty"`
+	MinProperties        *uint64               `json:"minProperties,omitempty"`
+	Default              json.RawMessage       `json:"default,omitempty"`
+	Example              json.RawMessage       `json:"example,omitempty"`
+	Deprecated           bool                  `json:"deprecated,omitempty"`
+}
+
+type RawProperty struct {
+	Name   string
+	Schema *RawSchema
+}
+
+type RawProperties []RawProperty
+
+func (p RawProperties) MarshalJSON() ([]byte, error) {
+	var e jx.Encoder
+	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
+}
+
+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
+	})
+}
+
+type AdditionalProperties struct {
+	Bool   bool
+	Schema RawSchema
+}
+
+func (p AdditionalProperties) MarshalJSON() ([]byte, error) {
+	if p.Bool {
+		return json.Marshal(p.Bool)
+	}
+	return json.Marshal(p.Schema)
+}
+
+func (p *AdditionalProperties) UnmarshalJSON(data []byte) error {
+	d := jx.DecodeBytes(data)
+	switch tt := d.Next(); tt {
+	case jx.Object:
+	case jx.Bool:
+		p.Bool = true
+		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
+}
+
+type Discriminator struct {
+	PropertyName string            `json:"propertyName"`
+	Mapping      map[string]string `json:"mapping,omitempty"`
+}
diff --git a/internal/oas/schema.go b/jsonschema/schema.go
similarity index 85%
rename from internal/oas/schema.go
rename to jsonschema/schema.go
index 3602f270..519af029 100644
--- a/internal/oas/schema.go
+++ b/jsonschema/schema.go
@@ -1,8 +1,8 @@
-package oas
+package jsonschema
 
 import "encoding/json"
 
-// SchemaType is an OpenAPI JSON Schema type.
+// SchemaType is a JSON Schema type.
 type SchemaType string
 
 const (
@@ -15,13 +15,7 @@ const (
 	Boolean SchemaType = "boolean"
 )
 
-// Discriminator discriminates types for OneOf, AllOf, AnyOf.
-type Discriminator struct {
-	PropertyName string
-	Mapping      map[string]string
-}
-
-// Schema is an OpenAPI JSON Schema.
+// Schema is a JSON Schema.
 type Schema struct {
 	Type        SchemaType
 	Format      Format // Schema format, optional.
@@ -70,7 +64,7 @@ func (s *Schema) AddExample(r json.RawMessage) {
 	}
 }
 
-// Property is an OpenAPI JSON Schema Object property.
+// Property is a JSON Schema Object property.
 type Property struct {
 	Name        string  // Property name.
 	Description string  // Property description.
diff --git a/spec_backcomp.go b/spec_backcomp.go
new file mode 100644
index 00000000..9317a083
--- /dev/null
+++ b/spec_backcomp.go
@@ -0,0 +1,87 @@
+package ogen
+
+import "github.com/ogen-go/ogen/jsonschema"
+
+func (s *Schema) ToJSONSchema() *jsonschema.RawSchema {
+	if s == nil {
+		return nil
+	}
+
+	convertMany := func(schemas []*Schema) []*jsonschema.RawSchema {
+		result := make([]*jsonschema.RawSchema, 0, len(schemas))
+		for _, s := range schemas {
+			result = append(result, s.ToJSONSchema())
+		}
+		return result
+	}
+
+	return &jsonschema.RawSchema{
+		Ref:                  s.Ref,
+		Description:          s.Description,
+		Type:                 s.Type,
+		Format:               s.Format,
+		Properties:           s.Properties.ToJSONSchema(),
+		AdditionalProperties: s.AdditionalProperties.ToJSONSchema(),
+		Required:             s.Required,
+		Items:                s.Items.ToJSONSchema(),
+		Nullable:             s.Nullable,
+		AllOf:                convertMany(s.AllOf),
+		OneOf:                convertMany(s.OneOf),
+		AnyOf:                convertMany(s.AnyOf),
+		Discriminator:        s.Discriminator.ToJSONSchema(),
+		Enum:                 s.Enum,
+		MultipleOf:           s.MultipleOf,
+		Maximum:              s.Maximum,
+		ExclusiveMaximum:     s.ExclusiveMaximum,
+		Minimum:              s.Minimum,
+		ExclusiveMinimum:     s.ExclusiveMinimum,
+		MaxLength:            s.MaxLength,
+		MinLength:            s.MinLength,
+		Pattern:              s.Pattern,
+		MaxItems:             s.MaxItems,
+		MinItems:             s.MinItems,
+		UniqueItems:          s.UniqueItems,
+		MaxProperties:        s.MaxProperties,
+		MinProperties:        s.MinProperties,
+		Default:              s.Default,
+		Example:              s.Example,
+		Deprecated:           s.Deprecated,
+	}
+}
+
+func (p Properties) ToJSONSchema() jsonschema.RawProperties {
+	result := make([]jsonschema.RawProperty, 0, len(p))
+	for _, prop := range p {
+		result = append(result, prop.ToJSONSchema())
+	}
+	return result
+}
+
+func (p Property) ToJSONSchema() jsonschema.RawProperty {
+	return jsonschema.RawProperty{
+		Name:   p.Name,
+		Schema: p.Schema.ToJSONSchema(),
+	}
+}
+
+func (p *AdditionalProperties) ToJSONSchema() *jsonschema.AdditionalProperties {
+	if p == nil {
+		return nil
+	}
+
+	return &jsonschema.AdditionalProperties{
+		Bool:   p.Bool,
+		Schema: *p.Schema.ToJSONSchema(),
+	}
+}
+
+func (d *Discriminator) ToJSONSchema() *jsonschema.Discriminator {
+	if d == nil {
+		return nil
+	}
+
+	return &jsonschema.Discriminator{
+		PropertyName: d.PropertyName,
+		Mapping:      d.Mapping,
+	}
+}


Test patch:
diff --git a/internal/ir/json_test.go b/internal/ir/json_test.go
index d71a8600..876e8f61 100644
--- a/internal/ir/json_test.go
+++ b/internal/ir/json_test.go
@@ -3,19 +3,18 @@ package ir
 import (
 	"testing"
 
+	"github.com/ogen-go/ogen/jsonschema"
 	"github.com/stretchr/testify/require"
-
-	"github.com/ogen-go/ogen/internal/oas"
 )
 
 func TestJSONFields_RequiredMask(t *testing.T) {
 	required := &Field{
-		Spec: &oas.Property{
+		Spec: &jsonschema.Property{
 			Required: true,
 		},
 	}
 	optional := &Field{
-		Spec: &oas.Property{
+		Spec: &jsonschema.Property{
 			Required: false,
 		},
 	}
diff --git a/internal/oas/parser/path_parser_test.go b/internal/oas/parser/path_parser_test.go
index 786ea587..716c5b29 100644
--- a/internal/oas/parser/path_parser_test.go
+++ b/internal/oas/parser/path_parser_test.go
@@ -6,18 +6,19 @@ import (
 	"github.com/stretchr/testify/require"
 
 	"github.com/ogen-go/ogen/internal/oas"
+	"github.com/ogen-go/ogen/jsonschema"
 )
 
 func TestPathParser(t *testing.T) {
 	var (
 		bar = &oas.Parameter{
 			Name:   "bar",
-			Schema: &oas.Schema{Type: oas.Integer},
+			Schema: &jsonschema.Schema{Type: jsonschema.Integer},
 			In:     oas.LocationPath,
 		}
 		baz = &oas.Parameter{
 			Name:   "baz",
-			Schema: &oas.Schema{Type: oas.String},
+			Schema: &jsonschema.Schema{Type: jsonschema.String},
 			In:     oas.LocationPath,
 		}
 	)
diff --git a/internal/oas/parser/schema_parser_test.go b/internal/oas/parser/schema_parser_test.go
deleted file mode 100644
index ad5c5610..00000000
--- a/internal/oas/parser/schema_parser_test.go
+++ /dev/null
@@ -1,276 +0,0 @@
-package parser
-
-import (
-	"testing"
-
-	"github.com/stretchr/testify/require"
-
-	"github.com/ogen-go/ogen"
-	"github.com/ogen-go/ogen/internal/oas"
-)
-
-func TestSchemaSimple(t *testing.T) {
-	parser := &schemaParser{
-		localRefs: make(map[string]*oas.Schema),
-	}
-
-	out, err := parser.Parse(&ogen.Schema{
-		Type: "object",
-		Properties: []ogen.Property{
-			{
-				Name:   "id",
-				Schema: &ogen.Schema{Type: "integer"},
-			},
-			{
-				Name:   "name",
-				Schema: &ogen.Schema{Type: "string"},
-			},
-		},
-		Required: []string{"id", "name"},
-	})
-	require.NoError(t, err)
-
-	expect := &oas.Schema{
-		Type: oas.Object,
-		Properties: []oas.Property{
-			{
-				Name:     "id",
-				Schema:   &oas.Schema{Type: oas.Integer},
-				Required: true,
-			},
-			{
-				Name:     "name",
-				Schema:   &oas.Schema{Type: oas.String},
-				Required: true,
-			},
-		},
-	}
-
-	require.Equal(t, expect, out)
-}
-
-func TestSchemaRecursive(t *testing.T) {
-	components := map[string]*ogen.Schema{
-		"Pet": {
-			Type: "object",
-			Properties: []ogen.Property{
-				{
-					Name:   "id",
-					Schema: &ogen.Schema{Type: "integer"},
-				},
-				{
-					Name:   "name",
-					Schema: &ogen.Schema{Type: "string"},
-				},
-				{
-					Name: "friends",
-					Schema: &ogen.Schema{
-						Type: "array",
-						Items: &ogen.Schema{
-							Ref: "#/components/schemas/Pet",
-						},
-					},
-				},
-			},
-			Required: []string{"id", "name", "friends"},
-		},
-	}
-
-	pet := &oas.Schema{
-		Type: oas.Object,
-		Ref:  "#/components/schemas/Pet",
-	}
-	pet.Properties = []oas.Property{
-		{
-			Name:     "id",
-			Schema:   &oas.Schema{Type: oas.Integer},
-			Required: true,
-		},
-		{
-			Name:     "name",
-			Schema:   &oas.Schema{Type: oas.String},
-			Required: true,
-		},
-		{
-			Name: "friends",
-			Schema: &oas.Schema{
-				Type: oas.Array,
-				Item: pet,
-			},
-			Required: true,
-		},
-	}
-
-	expectLocalRefs := map[string]*oas.Schema{
-		"#/components/schemas/Pet": {
-			Type: oas.Object,
-			Ref:  "#/components/schemas/Pet",
-			Properties: []oas.Property{
-				{
-					Name:     "id",
-					Schema:   &oas.Schema{Type: oas.Integer},
-					Required: true,
-				},
-				{
-					Name:     "name",
-					Schema:   &oas.Schema{Type: oas.String},
-					Required: true,
-				},
-				{
-					Name: "friends",
-					Schema: &oas.Schema{
-						Type: oas.Array,
-						Item: pet,
-					},
-					Required: true,
-				},
-			},
-		},
-	}
-
-	parser := &schemaParser{
-		components: components,
-		localRefs:  make(map[string]*oas.Schema),
-	}
-
-	out, err := parser.Parse(&ogen.Schema{
-		Ref: "#/components/schemas/Pet",
-	})
-	require.NoError(t, err)
-	require.Equal(t, expectLocalRefs, parser.localRefs)
-	require.Equal(t, pet, out)
-}
-
-func TestSchemaSideEffects(t *testing.T) {
-	expectSide := []*oas.Schema{
-		{
-			Type: oas.Object,
-			Properties: []oas.Property{
-				{
-					Name:     "name",
-					Schema:   &oas.Schema{Type: oas.String},
-					Required: true,
-				},
-				{
-					Name:     "age",
-					Schema:   &oas.Schema{Type: oas.Integer},
-					Required: true,
-				},
-				{
-					Name:     "id",
-					Schema:   &oas.Schema{Type: oas.Integer},
-					Required: true,
-				},
-			},
-		},
-	}
-
-	expect := &oas.Schema{
-		Type: oas.Object,
-		Properties: []oas.Property{
-			{
-				Name:     "name",
-				Schema:   &oas.Schema{Type: oas.String},
-				Required: true,
-			},
-			{
-				Name:     "owner",
-				Schema:   expectSide[0],
-				Required: true,
-			},
-		},
-	}
-
-	parser := &schemaParser{
-		localRefs: make(map[string]*oas.Schema),
-	}
-
-	out, err := parser.Parse(&ogen.Schema{
-		Type: "object",
-		Properties: []ogen.Property{
-			{
-				Name:   "name",
-				Schema: &ogen.Schema{Type: "string"},
-			},
-			{
-				Name: "owner",
-				Schema: &ogen.Schema{
-					Type: "object",
-					Properties: []ogen.Property{
-						{
-							Name:   "name",
-							Schema: &ogen.Schema{Type: "string"},
-						},
-						{
-							Name:   "age",
-							Schema: &ogen.Schema{Type: "integer"},
-						},
-						{
-							Name:   "id",
-							Schema: &ogen.Schema{Type: "integer"},
-						},
-					},
-					Required: []string{"name", "id", "age"},
-				},
-			},
-		},
-		Required: []string{"id", "name", "owner"},
-	})
-
-	require.NoError(t, err)
-	require.Equal(t, expect, out)
-}
-
-func TestSchemaReferencedArray(t *testing.T) {
-	components := map[string]*ogen.Schema{
-		"Pets": {
-			Type: "array",
-			Items: &ogen.Schema{
-				Type: "string",
-			},
-		},
-	}
-
-	pets := &oas.Schema{
-		Type: oas.Array,
-		Ref:  "#/components/schemas/Pets",
-		Item: &oas.Schema{Type: oas.String},
-	}
-
-	expectLocalRefs := map[string]*oas.Schema{
-		"#/components/schemas/Pets": pets,
-	}
-
-	expect := &oas.Schema{
-		Type: oas.Object,
-		Properties: []oas.Property{
-			{
-				Name:     "pets",
-				Schema:   pets,
-				Required: true,
-			},
-		},
-	}
-
-	parser := &schemaParser{
-		components: components,
-		localRefs:  make(map[string]*oas.Schema),
-	}
-
-	out, err := parser.Parse(&ogen.Schema{
-		Type: "object",
-		Properties: []ogen.Property{
-			{
-				Name: "pets",
-				Schema: &ogen.Schema{
-					Ref: "#/components/schemas/Pets",
-				},
-			},
-		},
-		Required: []string{"pets"},
-	})
-
-	require.NoError(t, err)
-	require.Equal(t, expectLocalRefs, parser.localRefs)
-	require.Equal(t, expect, out)
-}
diff --git a/jsonschema/parser_test.go b/jsonschema/parser_test.go
new file mode 100644
index 00000000..174036f8
--- /dev/null
+++ b/jsonschema/parser_test.go
@@ -0,0 +1,286 @@
+package jsonschema
+
+import (
+	"strings"
+	"testing"
+
+	"github.com/go-faster/errors"
+	"github.com/stretchr/testify/require"
+)
+
+type components map[string]*RawSchema
+
+func (c components) ResolveReference(ref string) (*RawSchema, error) {
+	const prefix = "#/components/schemas/"
+	if !strings.HasPrefix(ref, prefix) {
+		return nil, errors.Errorf("invalid schema reference %q", ref)
+	}
+
+	name := strings.TrimPrefix(ref, prefix)
+	s, ok := c[name]
+	if !ok {
+		return nil, errors.New("schema not found")
+	}
+
+	return s, nil
+}
+
+func TestSchemaSimple(t *testing.T) {
+	parser := NewParser(Settings{})
+
+	out, err := parser.Parse(&RawSchema{
+		Type: "object",
+		Properties: []RawProperty{
+			{
+				Name:   "id",
+				Schema: &RawSchema{Type: "integer"},
+			},
+			{
+				Name:   "name",
+				Schema: &RawSchema{Type: "string"},
+			},
+		},
+		Required: []string{"id", "name"},
+	})
+	require.NoError(t, err)
+
+	expect := &Schema{
+		Type: Object,
+		Properties: []Property{
+			{
+				Name:     "id",
+				Schema:   &Schema{Type: Integer},
+				Required: true,
+			},
+			{
+				Name:     "name",
+				Schema:   &Schema{Type: String},
+				Required: true,
+			},
+		},
+	}
+
+	require.Equal(t, expect, out)
+}
+
+func TestSchemaRecursive(t *testing.T) {
+	components := components{
+		"Pet": {
+			Type: "object",
+			Properties: []RawProperty{
+				{
+					Name:   "id",
+					Schema: &RawSchema{Type: "integer"},
+				},
+				{
+					Name:   "name",
+					Schema: &RawSchema{Type: "string"},
+				},
+				{
+					Name: "friends",
+					Schema: &RawSchema{
+						Type: "array",
+						Items: &RawSchema{
+							Ref: "#/components/schemas/Pet",
+						},
+					},
+				},
+			},
+			Required: []string{"id", "name", "friends"},
+		},
+	}
+
+	pet := &Schema{
+		Type: Object,
+		Ref:  "#/components/schemas/Pet",
+	}
+	pet.Properties = []Property{
+		{
+			Name:     "id",
+			Schema:   &Schema{Type: Integer},
+			Required: true,
+		},
+		{
+			Name:     "name",
+			Schema:   &Schema{Type: String},
+			Required: true,
+		},
+		{
+			Name: "friends",
+			Schema: &Schema{
+				Type: Array,
+				Item: pet,
+			},
+			Required: true,
+		},
+	}
+
+	expectRefcache := map[string]*Schema{
+		"#/components/schemas/Pet": {
+			Type: Object,
+			Ref:  "#/components/schemas/Pet",
+			Properties: []Property{
+				{
+					Name:     "id",
+					Schema:   &Schema{Type: Integer},
+					Required: true,
+				},
+				{
+					Name:     "name",
+					Schema:   &Schema{Type: String},
+					Required: true,
+				},
+				{
+					Name: "friends",
+					Schema: &Schema{
+						Type: Array,
+						Item: pet,
+					},
+					Required: true,
+				},
+			},
+		},
+	}
+
+	parser := NewParser(Settings{
+		Resolver: components,
+	})
+
+	out, err := parser.Parse(&RawSchema{
+		Ref: "#/components/schemas/Pet",
+	})
+	require.NoError(t, err)
+	require.Equal(t, expectRefcache, parser.refcache)
+	require.Equal(t, pet, out)
+}
+
+func TestSchemaSideEffects(t *testing.T) {
+	expectSide := []*Schema{
+		{
+			Type: Object,
+			Properties: []Property{
+				{
+					Name:     "name",
+					Schema:   &Schema{Type: String},
+					Required: true,
+				},
+				{
+					Name:     "age",
+					Schema:   &Schema{Type: Integer},
+					Required: true,
+				},
+				{
+					Name:     "id",
+					Schema:   &Schema{Type: Integer},
+					Required: true,
+				},
+			},
+		},
+	}
+
+	expect := &Schema{
+		Type: Object,
+		Properties: []Property{
+			{
+				Name:     "name",
+				Schema:   &Schema{Type: String},
+				Required: true,
+			},
+			{
+				Name:     "owner",
+				Schema:   expectSide[0],
+				Required: true,
+			},
+		},
+	}
+
+	parser := NewParser(Settings{})
+
+	out, err := parser.Parse(&RawSchema{
+		Type: "object",
+		Properties: []RawProperty{
+			{
+				Name:   "name",
+				Schema: &RawSchema{Type: "string"},
+			},
+			{
+				Name: "owner",
+				Schema: &RawSchema{
+					Type: "object",
+					Properties: []RawProperty{
+						{
+							Name:   "name",
+							Schema: &RawSchema{Type: "string"},
+						},
+						{
+							Name:   "age",
+							Schema: &RawSchema{Type: "integer"},
+						},
+						{
+							Name:   "id",
+							Schema: &RawSchema{Type: "integer"},
+						},
+					},
+					Required: []string{"name", "id", "age"},
+				},
+			},
+		},
+		Required: []string{"id", "name", "owner"},
+	})
+
+	require.NoError(t, err)
+	require.Equal(t, expect, out)
+}
+
+func TestSchemaReferencedArray(t *testing.T) {
+	components := components{
+		"Pets": {
+			Type: "array",
+			Items: &RawSchema{
+				Type: "string",
+			},
+		},
+	}
+
+	pets := &Schema{
+		Type: Array,
+		Ref:  "#/components/schemas/Pets",
+		Item: &Schema{Type: String},
+	}
+
+	expectRefcache := map[string]*Schema{
+		"#/components/schemas/Pets": pets,
+	}
+
+	expect := &Schema{
+		Type: Object,
+		Properties: []Property{
+			{
+				Name:     "pets",
+				Schema:   pets,
+				Required: true,
+			},
+		},
+	}
+
+	parser := NewParser(Settings{
+		Resolver: components,
+	})
+
+	out, err := parser.Parse(&RawSchema{
+		Type: "object",
+		Properties: []RawProperty{
+			{
+				Name: "pets",
+				Schema: &RawSchema{
+					Ref: "#/components/schemas/Pets",
+				},
+			},
+		},
+		Required: []string{"pets"},
+	})
+
+	require.NoError(t, err)
+	require.Equal(t, expectRefcache, parser.refcache)
+	require.Equal(t, expect, out)
+}