diff --git a/compiler.go b/compiler.go index 7f809d9..829f9b7 100644 --- a/compiler.go +++ b/compiler.go @@ -98,6 +98,16 @@ func (c *Compiler) RegisterVocabulary(vocab *Vocabulary) { c.roots.vocabularies[vocab.URL] = vocab } +// AssertVocabs always enables user-defined vocabularies assertions. +// +// Default Behavior: +// for draft-07: enabled. +// for draft/2019-09: disabled unless metaschema enables a vocabulary. +// for draft/2020-12: disabled unless metaschema enables a vocabulary. +func (c *Compiler) AssertVocabs() { + c.roots.assertVocabs = true +} + // AddResource adds schema resource which gets used later in reference // resolution. // diff --git a/compiler_test.go b/compiler_test.go index e803609..54ebf3d 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -110,3 +110,21 @@ func TestCompileNonStd(t *testing.T) { t.Fatal(err) } } + +func TestCustomVocabValidation(t *testing.T) { + schema, err := jsonschema.UnmarshalJSON(strings.NewReader(`{"uniqueKeys": 1}`)) + if err != nil { + t.Fatal(err) + } + + c := jsonschema.NewCompiler() + c.AssertVocabs() + c.RegisterVocabulary(uniqueKeysVocab()) + if err := c.AddResource("schema.json", schema); err != nil { + t.Fatal(err) + } + _, err = c.Compile("schema.json") + if err == nil { + t.Fatal("exception compilation failure") + } +} diff --git a/draft.go b/draft.go index b165a93..a0fa51e 100644 --- a/draft.go +++ b/draft.go @@ -273,19 +273,49 @@ func (d *dialect) hasVocab(name string) bool { return slices.Contains(d.draft.defaultVocabs, name) } -func (d *dialect) getSchema() *Schema { +func (d *dialect) activeVocabs(assertVocabs bool, vocabularies map[string]*Vocabulary) []string { + if len(vocabularies) == 0 { + return d.vocabs + } + if d.draft.version < 2019 { + assertVocabs = true + } + if !assertVocabs { + return d.vocabs + } + var vocabs []string if d.vocabs == nil { + vocabs = slices.Clone(d.draft.defaultVocabs) + } else { + vocabs = slices.Clone(d.vocabs) + } + for vocab := range vocabularies { + if !slices.Contains(vocabs, vocab) { + vocabs = append(vocabs, vocab) + } + } + return vocabs +} + +func (d *dialect) getSchema(assertVocabs bool, vocabularies map[string]*Vocabulary) *Schema { + vocabs := d.activeVocabs(assertVocabs, vocabularies) + if vocabs == nil { return d.draft.sch } - // TODO: support custom vocabulary + var allOf []*Schema - for _, vocab := range d.vocabs { + for _, vocab := range vocabs { sch := d.draft.allVocabs[vocab] + if sch == nil { + if v, ok := vocabularies[vocab]; ok { + sch = v.Schema + } + } if sch != nil { allOf = append(allOf, sch) } } - if !slices.Contains(d.vocabs, "core") { + if !slices.Contains(vocabs, "core") { sch := d.draft.allVocabs["core"] if sch == nil { sch = d.draft.sch diff --git a/example_vocab_test.go b/example_vocab_test.go index b03994a..68ecd5d 100644 --- a/example_vocab_test.go +++ b/example_vocab_test.go @@ -116,6 +116,7 @@ func Example_vocab_uniquekeys() { } c := jsonschema.NewCompiler() + c.AssertVocabs() c.RegisterVocabulary(uniqueKeysVocab()) if err := c.AddResource("schema.json", schema); err != nil { log.Fatal(err) diff --git a/objcompiler.go b/objcompiler.go index 34ec722..5f998c7 100644 --- a/objcompiler.go +++ b/objcompiler.go @@ -65,8 +65,13 @@ func (c *objCompiler) compile(s *Schema) error { } // vocabularies - for _, vocab := range c.c.roots.vocabularies { - ext, err := vocab.Compile(&CompilerContext{}, c.obj) + vocabs := c.res.dialect.activeVocabs(c.c.roots.assertVocabs, c.c.roots.vocabularies) + for _, vocab := range vocabs { + v := c.c.roots.vocabularies[vocab] + if v == nil { + continue + } + ext, err := v.Compile(&CompilerContext{}, c.obj) if err != nil { return err } diff --git a/root.go b/root.go index 0dceacc..3480127 100644 --- a/root.go +++ b/root.go @@ -265,16 +265,6 @@ func (r *root) collectAnchors(sch any, schPtr jsonPointer, res *resource) error return nil } -func (r *root) validate(ptr jsonPointer, v any, regexpEngine RegexpEngine) error { - dialect := r.resource(ptr).dialect - up := urlPtr{r.url, ptr} - meta := dialect.getSchema() - if err := meta.validate(v, regexpEngine, meta, r.resources); err != nil { - return &SchemaValidationError{URL: up.String(), Err: err} - } - return nil -} - func (r *root) clone() *root { processed := map[jsonPointer]struct{}{} for k := range r.subschemasProcessed { diff --git a/roots.go b/roots.go index 8cf9dc4..23066a9 100644 --- a/roots.go +++ b/roots.go @@ -11,6 +11,7 @@ type roots struct { loader defaultLoader regexpEngine RegexpEngine vocabularies map[string]*Vocabulary + assertVocabs bool } func newRoots() *roots { @@ -49,7 +50,7 @@ func (rr *roots) addRoot(u url, doc any) (*root, error) { } if !strings.HasPrefix(u.String(), "http://json-schema.org/") && !strings.HasPrefix(u.String(), "https://json-schema.org/") { - if err := r.validate("", doc, rr.regexpEngine); err != nil { + if err := rr.validate(r, doc, ""); err != nil { return nil, err } } @@ -82,13 +83,23 @@ func (rr *roots) ensureSubschema(up urlPtr) error { if err := rClone.addSubschema(&rr.loader, up.ptr); err != nil { return err } - if err := rClone.validate(up.ptr, v, rr.regexpEngine); err != nil { + if err := rr.validate(rClone, v, up.ptr); err != nil { return err } rr.roots[r.url] = rClone return nil } +func (rr *roots) validate(r *root, v any, ptr jsonPointer) error { + dialect := r.resource(ptr).dialect + meta := dialect.getSchema(rr.assertVocabs, rr.vocabularies) + if err := meta.validate(v, rr.regexpEngine, meta, r.resources, rr.assertVocabs, rr.vocabularies); err != nil { + up := urlPtr{r.url, ptr} + return &SchemaValidationError{URL: up.String(), Err: err} + } + return nil +} + // -- type InvalidMetaSchemaURLError struct { diff --git a/validator.go b/validator.go index d14ae17..e9c4430 100644 --- a/validator.go +++ b/validator.go @@ -12,10 +12,10 @@ import ( ) func (sch *Schema) Validate(v any) error { - return sch.validate(v, nil, nil, nil) + return sch.validate(v, nil, nil, nil, false, nil) } -func (sch *Schema) validate(v any, regexpEngine RegexpEngine, meta *Schema, resources map[jsonPointer]*resource) error { +func (sch *Schema) validate(v any, regexpEngine RegexpEngine, meta *Schema, resources map[jsonPointer]*resource, assertVocabs bool, vocabularies map[string]*Vocabulary) error { vd := validator{ v: v, vloc: make([]string, 0, 8), @@ -27,6 +27,8 @@ func (sch *Schema) validate(v any, regexpEngine RegexpEngine, meta *Schema, reso regexpEngine: regexpEngine, meta: meta, resources: resources, + assertVocabs: assertVocabs, + vocabularies: vocabularies, } if _, err := vd.validate(); err != nil { verr := err.(*ValidationError) @@ -58,8 +60,10 @@ type validator struct { regexpEngine RegexpEngine // meta validation - meta *Schema // set only when validating with metaschema - resources map[jsonPointer]*resource // resources which should be validated with their dialect + meta *Schema // set only when validating with metaschema + resources map[jsonPointer]*resource // resources which should be validated with their dialect + assertVocabs bool + vocabularies map[string]*Vocabulary } func (vd *validator) validate() (*uneval, error) { @@ -283,10 +287,10 @@ func (vd *validator) objValidate(obj map[string]any) { sch, meta, resources := s.PropertyNames, vd.meta, vd.resources res := vd.metaResource(sch) if res != nil { - meta = res.dialect.getSchema() + meta = res.dialect.getSchema(vd.assertVocabs, vd.vocabularies) sch = meta } - if err := sch.validate(pname, vd.regexpEngine, meta, resources); err != nil { + if err := sch.validate(pname, vd.regexpEngine, meta, resources, vd.assertVocabs, vd.vocabularies); err != nil { verr := err.(*ValidationError) verr.SchemaURL = s.PropertyNames.Location verr.ErrorKind = kind.PropertyNames(pname) @@ -493,10 +497,10 @@ func (vd *validator) strValidate(str string) { sch, meta, resources := s.ContentSchema, vd.meta, vd.resources res := vd.metaResource(sch) if res != nil { - meta = res.dialect.getSchema() + meta = res.dialect.getSchema(vd.assertVocabs, vd.vocabularies) sch = meta } - if err = sch.validate(*deserialized, vd.regexpEngine, meta, resources); err != nil { + if err = sch.validate(*deserialized, vd.regexpEngine, meta, resources, vd.assertVocabs, vd.vocabularies); err != nil { verr := err.(*ValidationError) verr.SchemaURL = s.Location verr.ErrorKind = kind.ContentSchema{} @@ -663,6 +667,8 @@ func (vd *validator) validateSelf(sch *Schema, refKw string, boolResult bool) er regexpEngine: vd.regexpEngine, meta: vd.meta, resources: vd.resources, + assertVocabs: vd.assertVocabs, + vocabularies: vd.vocabularies, } subvd.handleMeta() uneval, err := subvd.validate() @@ -687,6 +693,8 @@ func (vd *validator) validateVal(sch *Schema, v any, vtok string) error { regexpEngine: vd.regexpEngine, meta: vd.meta, resources: vd.resources, + assertVocabs: vd.assertVocabs, + vocabularies: vd.vocabularies, } subvd.handleMeta() _, err := subvd.validate() @@ -710,7 +718,7 @@ func (vd *validator) handleMeta() { if res == nil { return } - sch := res.dialect.getSchema() + sch := res.dialect.getSchema(vd.assertVocabs, vd.vocabularies) vd.meta = sch vd.sch = sch }