Skip to content

Commit da004e9

Browse files
feat: add $schema, $defs keywords and fix remaining issues
- Add $schema keyword to Schema struct for per-schema dialect declaration - Add $defs keyword (Schemas map) for local reusable schema definitions, with full support: struct, marshal, unmarshal, IsEmpty, JSONLookup, validate (recurse), loader (resolve refs), transform (recurse) - Fix jsonSchemaDialect URI validation to require a scheme - Refactor discriminator resolution into shared helper to eliminate code duplication between oneOf and anyOf paths - Regenerate docs Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent b00ede4 commit da004e9

File tree

5 files changed

+95
-70
lines changed

5 files changed

+95
-70
lines changed

.github/docs/openapi3.txt

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1693,7 +1693,9 @@ type Schema struct {
16931693
DependentRequired map[string][]string `json:"dependentRequired,omitempty" yaml:"dependentRequired,omitempty"`
16941694

16951695
// JSON Schema 2020-12 core keywords
1696-
Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"`
1696+
Defs Schemas `json:"$defs,omitempty" yaml:"$defs,omitempty"`
1697+
SchemaDialect string `json:"$schema,omitempty" yaml:"$schema,omitempty"`
1698+
Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"`
16971699

16981700
// JSON Schema 2020-12 identity/referencing keywords
16991701
SchemaID string `json:"$id,omitempty" yaml:"$id,omitempty"`

openapi3/loader.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,12 @@ func (loader *Loader) resolveSchemaRef(doc *T, component *SchemaRef, documentPat
10051005
return err
10061006
}
10071007
}
1008+
for _, name := range componentNames(value.Defs) {
1009+
v := value.Defs[name]
1010+
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
1011+
return err
1012+
}
1013+
}
10081014
if v := value.PropertyNames; v != nil {
10091015
if err := loader.resolveSchemaRef(doc, v, documentPath, visited); err != nil {
10101016
return err

openapi3/openapi3.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,9 +254,13 @@ func (doc *T) Validate(ctx context.Context, opts ...ValidationOption) error {
254254

255255
// OpenAPI 3.1 jsonSchemaDialect validation
256256
if doc.JSONSchemaDialect != "" {
257-
if _, err := url.Parse(doc.JSONSchemaDialect); err != nil {
257+
u, err := url.Parse(doc.JSONSchemaDialect)
258+
if err != nil {
258259
return fmt.Errorf("invalid jsonSchemaDialect: %w", err)
259260
}
261+
if u.Scheme == "" {
262+
return fmt.Errorf("invalid jsonSchemaDialect: must be an absolute URI with a scheme")
263+
}
260264
}
261265

262266
// OpenAPI 3.1 webhooks validation

openapi3/schema.go

Lines changed: 79 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,9 @@ type Schema struct {
157157
DependentRequired map[string][]string `json:"dependentRequired,omitempty" yaml:"dependentRequired,omitempty"`
158158

159159
// JSON Schema 2020-12 core keywords
160-
Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"`
160+
Defs Schemas `json:"$defs,omitempty" yaml:"$defs,omitempty"`
161+
SchemaDialect string `json:"$schema,omitempty" yaml:"$schema,omitempty"`
162+
Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"`
161163

162164
// JSON Schema 2020-12 identity/referencing keywords
163165
SchemaID string `json:"$id,omitempty" yaml:"$id,omitempty"`
@@ -663,6 +665,12 @@ func (schema Schema) MarshalYAML() (any, error) {
663665
if x := schema.DependentRequired; len(x) != 0 {
664666
m["dependentRequired"] = x
665667
}
668+
if x := schema.Defs; len(x) != 0 {
669+
m["$defs"] = x
670+
}
671+
if x := schema.SchemaDialect; x != "" {
672+
m["$schema"] = x
673+
}
666674
if x := schema.Comment; x != "" {
667675
m["$comment"] = x
668676
}
@@ -766,6 +774,8 @@ func (schema *Schema) UnmarshalJSON(data []byte) error {
766774
delete(x.Extensions, "then")
767775
delete(x.Extensions, "else")
768776
delete(x.Extensions, "dependentRequired")
777+
delete(x.Extensions, "$defs")
778+
delete(x.Extensions, "$schema")
769779
delete(x.Extensions, "$comment")
770780
delete(x.Extensions, "$id")
771781
delete(x.Extensions, "$anchor")
@@ -950,6 +960,10 @@ func (schema Schema) JSONLookup(token string) (any, error) {
950960
}
951961
case "dependentRequired":
952962
return schema.DependentRequired, nil
963+
case "$defs":
964+
return schema.Defs, nil
965+
case "$schema":
966+
return schema.SchemaDialect, nil
953967
case "$comment":
954968
return schema.Comment, nil
955969
case "$id":
@@ -1363,7 +1377,10 @@ func (schema *Schema) IsEmpty() bool {
13631377
if len(schema.DependentRequired) != 0 {
13641378
return false
13651379
}
1366-
if schema.Comment != "" {
1380+
if len(schema.Defs) != 0 {
1381+
return false
1382+
}
1383+
if schema.SchemaDialect != "" || schema.Comment != "" {
13671384
return false
13681385
}
13691386
if schema.SchemaID != "" || schema.Anchor != "" || schema.DynamicRef != "" || schema.DynamicAnchor != "" {
@@ -1637,6 +1654,18 @@ func (schema *Schema) validate(ctx context.Context, stack []*Schema) ([]*Schema,
16371654
return stack, err
16381655
}
16391656
}
1657+
for _, name := range componentNames(schema.Defs) {
1658+
ref := schema.Defs[name]
1659+
v := ref.Value
1660+
if v == nil {
1661+
return stack, foundUnresolvedRef(ref.Ref)
1662+
}
1663+
1664+
var err error
1665+
if stack, err = v.validate(ctx, stack); err != nil {
1666+
return stack, err
1667+
}
1668+
}
16401669
if ref := schema.PropertyNames; ref != nil {
16411670
v := ref.Value
16421671
if v == nil {
@@ -1944,41 +1973,54 @@ func (schema *Schema) visitNotOperation(settings *schemaValidationSettings, valu
19441973

19451974
// If the XOF operations pass successfully, abort further run of validation, as they will already be satisfied (unless the schema
19461975
// itself is badly specified
1976+
// resolveDiscriminatorRef resolves the discriminator reference for oneOf/anyOf validation.
1977+
// Returns the discriminator ref string and any error encountered during resolution.
1978+
func (schema *Schema) resolveDiscriminatorRef(value any) (string, error) {
1979+
if schema.Discriminator == nil {
1980+
return "", nil
1981+
}
1982+
pn := schema.Discriminator.PropertyName
1983+
valuemap, okcheck := value.(map[string]any)
1984+
if !okcheck {
1985+
return "", nil
1986+
}
1987+
discriminatorVal, okcheck := valuemap[pn]
1988+
if !okcheck {
1989+
return "", &SchemaError{
1990+
Schema: schema,
1991+
SchemaField: "discriminator",
1992+
Reason: fmt.Sprintf("input does not contain the discriminator property %q", pn),
1993+
}
1994+
}
1995+
1996+
discriminatorValString, okcheck := discriminatorVal.(string)
1997+
if !okcheck {
1998+
return "", &SchemaError{
1999+
Value: discriminatorVal,
2000+
Schema: schema,
2001+
SchemaField: "discriminator",
2002+
Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn),
2003+
}
2004+
}
2005+
2006+
if discriminatorRef, okcheck := schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
2007+
return "", &SchemaError{
2008+
Value: discriminatorVal,
2009+
Schema: schema,
2010+
SchemaField: "discriminator",
2011+
Reason: fmt.Sprintf("discriminator property %q has invalid value", pn),
2012+
}
2013+
} else {
2014+
return discriminatorRef, nil
2015+
}
2016+
}
2017+
19472018
func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, value any) (err error, run bool) {
19482019
var visitedOneOf, visitedAnyOf, visitedAllOf bool
19492020
if v := schema.OneOf; len(v) > 0 {
1950-
var discriminatorRef string
1951-
if schema.Discriminator != nil {
1952-
pn := schema.Discriminator.PropertyName
1953-
if valuemap, okcheck := value.(map[string]any); okcheck {
1954-
discriminatorVal, okcheck := valuemap[pn]
1955-
if !okcheck {
1956-
return &SchemaError{
1957-
Schema: schema,
1958-
SchemaField: "discriminator",
1959-
Reason: fmt.Sprintf("input does not contain the discriminator property %q", pn),
1960-
}, false
1961-
}
1962-
1963-
discriminatorValString, okcheck := discriminatorVal.(string)
1964-
if !okcheck {
1965-
return &SchemaError{
1966-
Value: discriminatorVal,
1967-
Schema: schema,
1968-
SchemaField: "discriminator",
1969-
Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn),
1970-
}, false
1971-
}
1972-
1973-
if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
1974-
return &SchemaError{
1975-
Value: discriminatorVal,
1976-
Schema: schema,
1977-
SchemaField: "discriminator",
1978-
Reason: fmt.Sprintf("discriminator property %q has invalid value", pn),
1979-
}, false
1980-
}
1981-
}
2021+
discriminatorRef, err := schema.resolveDiscriminatorRef(value)
2022+
if err != nil {
2023+
return err, false
19822024
}
19832025

19842026
var (
@@ -2040,38 +2082,9 @@ func (schema *Schema) visitXOFOperations(settings *schemaValidationSettings, val
20402082
}
20412083

20422084
if v := schema.AnyOf; len(v) > 0 {
2043-
var discriminatorRef string
2044-
if schema.Discriminator != nil {
2045-
pn := schema.Discriminator.PropertyName
2046-
if valuemap, okcheck := value.(map[string]any); okcheck {
2047-
discriminatorVal, okcheck := valuemap[pn]
2048-
if !okcheck {
2049-
return &SchemaError{
2050-
Schema: schema,
2051-
SchemaField: "discriminator",
2052-
Reason: fmt.Sprintf("input does not contain the discriminator property %q", pn),
2053-
}, false
2054-
}
2055-
2056-
discriminatorValString, okcheck := discriminatorVal.(string)
2057-
if !okcheck {
2058-
return &SchemaError{
2059-
Value: discriminatorVal,
2060-
Schema: schema,
2061-
SchemaField: "discriminator",
2062-
Reason: fmt.Sprintf("value of discriminator property %q is not a string", pn),
2063-
}, false
2064-
}
2065-
2066-
if discriminatorRef, okcheck = schema.Discriminator.Mapping[discriminatorValString]; len(schema.Discriminator.Mapping) > 0 && !okcheck {
2067-
return &SchemaError{
2068-
Value: discriminatorVal,
2069-
Schema: schema,
2070-
SchemaField: "discriminator",
2071-
Reason: fmt.Sprintf("discriminator property %q has invalid value", pn),
2072-
}, false
2073-
}
2074-
}
2085+
discriminatorRef, err := schema.resolveDiscriminatorRef(value)
2086+
if err != nil {
2087+
return err, false
20752088
}
20762089

20772090
var (

openapi3/schema_jsonschema_validator.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ func transformOpenAPIToJSONSchema(schema map[string]any) {
129129
}
130130
}
131131

132-
// Transform schema maps (properties, patternProperties, dependentSchemas)
133-
for _, key := range []string{"properties", "patternProperties", "dependentSchemas"} {
132+
// Transform schema maps (properties, patternProperties, dependentSchemas, $defs)
133+
for _, key := range []string{"properties", "patternProperties", "dependentSchemas", "$defs"} {
134134
if props, ok := schema[key].(map[string]any); ok {
135135
for _, propVal := range props {
136136
if propSchema, ok := propVal.(map[string]any); ok {

0 commit comments

Comments
 (0)