@@ -216,11 +216,12 @@ export interface ISchemaCollector {
216216
217217export interface IEvaluationContext {
218218 readonly schemaDraft : SchemaDraft ;
219+ readonly explicitSchemaDraft ?: SchemaDraft ;
219220 readonly activeVocabularies ?: Vocabularies ;
220221}
221222
222223class EvaluationContext implements IEvaluationContext {
223- constructor ( public readonly schemaDraft : SchemaDraft , public readonly activeVocabularies ?: Vocabularies ) {
224+ constructor ( public readonly schemaDraft : SchemaDraft , public readonly activeVocabularies ?: Vocabularies , public readonly explicitSchemaDraft ?: SchemaDraft ) {
224225 }
225226}
226227
@@ -382,7 +383,8 @@ export class JSONDocument {
382383 public validate ( textDocument : TextDocument , schema : JSONSchema | undefined , severity : DiagnosticSeverity = DiagnosticSeverity . Warning , schemaDraft ?: SchemaDraft , activeVocabularies ?: Vocabularies ) : Diagnostic [ ] | undefined {
383384 if ( this . root && schema ) {
384385 const validationResult = new ValidationResult ( ) ;
385- const context = new EvaluationContext ( schemaDraft ?? getSchemaDraft ( schema ) , activeVocabularies ) ;
386+ const { explicitDraft, effectiveDraft } = resolveDrafts ( schema , schemaDraft ) ;
387+ const context = new EvaluationContext ( effectiveDraft , activeVocabularies , explicitDraft ) ;
386388 const schemaStack : JSONSchema [ ] = [ ] ;
387389 const schemaRoots : JSONSchema [ ] = [ schema ] ;
388390 validate ( this . root , schema , validationResult , NoOpSchemaCollector . instance , context , schemaStack , schemaRoots ) ;
@@ -397,8 +399,8 @@ export class JSONDocument {
397399 public getMatchingSchemas ( schema : JSONSchema , focusOffset : number = - 1 , exclude ?: ASTNode , activeVocabularies ?: Vocabularies ) : IApplicableSchema [ ] {
398400 if ( this . root && schema ) {
399401 const matchingSchemas = new SchemaCollector ( focusOffset , exclude ) ;
400- const schemaDraft = getSchemaDraft ( schema ) ;
401- const context = new EvaluationContext ( schemaDraft , activeVocabularies ) ;
402+ const { explicitDraft , effectiveDraft } = resolveDrafts ( schema ) ;
403+ const context = new EvaluationContext ( effectiveDraft , activeVocabularies , explicitDraft ) ;
402404 const schemaStack : JSONSchema [ ] = [ ] ;
403405 const schemaRoots : JSONSchema [ ] = [ schema ] ;
404406 validate ( this . root , schema , new ValidationResult ( ) , matchingSchemas , context , schemaStack , schemaRoots ) ;
@@ -407,14 +409,29 @@ export class JSONDocument {
407409 return [ ] ;
408410 }
409411}
410- function getSchemaDraft ( schema : JSONSchema , fallBack = SchemaDraft . v2020_12 ) {
411- let schemaId = schema . $schema ;
412- if ( schemaId ) {
413- return getSchemaDraftFromId ( schemaId ) ?? fallBack ;
414- }
415- return fallBack ;
412+
413+ /*
414+ * Resolves the schema draft versions for validation.
415+ * - explicitDraft: the draft explicitly declared via $schema or passed as an override.
416+ * Undefined when no $schema is present and no override is provided.
417+ * - effectiveDraft: the draft used for keyword gating, defaulting to 2020-12.
418+ * These are tracked separately so that behaviors like format assertion can
419+ * distinguish "explicitly 2020-12" (annotation-only per spec) from
420+ * "no $schema, defaulting to 2020-12" (asserts for backward compatibility).
421+ */
422+ function resolveDrafts ( schema : JSONSchema , schemaDraftOverride ?: SchemaDraft ) : { explicitDraft : SchemaDraft | undefined ; effectiveDraft : SchemaDraft } {
423+ const explicitDraft = schemaDraftOverride ?? ( schema . $schema ? getSchemaDraftFromId ( schema . $schema ) : undefined ) ;
424+ const effectiveDraft = explicitDraft ?? SchemaDraft . v2020_12 ;
425+ return { explicitDraft, effectiveDraft } ;
416426}
417427
428+ // Keywords introduced in 2019-09 (not available in draft-07 and earlier)
429+ const keywords201909 = new Set ( [
430+ 'dependentRequired' , 'dependentSchemas' ,
431+ 'unevaluatedProperties' , 'unevaluatedItems' ,
432+ 'minContains' , 'maxContains'
433+ ] ) ;
434+
418435function validate ( n : ASTNode | undefined , schema : JSONSchema , validationResult : ValidationResult , matchingSchemas : ISchemaCollector , context : IEvaluationContext , schemaStack : JSONSchema [ ] , schemaRoots : JSONSchema [ ] ) : void {
419436
420437 if ( ! n || ! matchingSchemas . include ( n ) ) {
@@ -455,14 +472,29 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
455472 schemaStack . push ( schema ) ;
456473
457474 const enabled = ( keyword : string ) => {
458- // Special case for 'dependencies' which is a core keyword in draft-04 through draft-07 but moved to
459- // dependentSchemas and dependentRequired in later versions.
460- if ( keyword === 'dependencies' )
461- {
475+ // Draft-based keyword gating: keywords only exist in certain drafts.
476+ // 'dependencies' was replaced by dependentRequired/dependentSchemas in 2019-09
477+ if ( keyword === 'dependencies' ) {
462478 return context . schemaDraft <= SchemaDraft . v7 ;
463479 }
464-
465- return isKeywordEnabled ( keyword , context . activeVocabularies ) ;
480+
481+ // Keywords introduced in 2019-09 (not available in draft-07 and earlier)
482+ if ( keywords201909 . has ( keyword ) ) {
483+ return context . schemaDraft >= SchemaDraft . v2019_09 ;
484+ }
485+
486+ // 'prefixItems' was introduced in 2020-12
487+ if ( keyword === 'prefixItems' ) {
488+ return context . schemaDraft >= SchemaDraft . v2020_12 ;
489+ }
490+
491+ // Vocabulary-based filtering only applies for 2019-09+ (vocabulary is not a concept in older drafts)
492+ if ( context . schemaDraft >= SchemaDraft . v2019_09 && context . activeVocabularies ) {
493+ return isKeywordEnabled ( keyword , context . activeVocabularies ) ;
494+ }
495+
496+ // No vocabulary info or pre-2019-09: enable all draft-appropriate keywords
497+ return true ;
466498 } ;
467499
468500 _validateNode ( ) ;
@@ -859,7 +891,7 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
859891 }
860892
861893 // Only validate format if format-assertion vocabulary is active (not annotation-only)
862- if ( schema . format && enabled ( 'format' ) && isFormatAssertionEnabled ( context . activeVocabularies ) ) {
894+ if ( schema . format && enabled ( 'format' ) && isFormatAssertionEnabled ( context . activeVocabularies , context . explicitSchemaDraft ) ) {
863895 switch ( schema . format ) {
864896 case 'uri' :
865897 case 'uri-reference' : {
@@ -1055,7 +1087,7 @@ function validate(n: ASTNode | undefined, schema: JSONSchema, validationResult:
10551087 for ( const propertyName of schema . required ) {
10561088 if ( ! seenKeys [ propertyName ] ) {
10571089 validationResult . problems . push ( {
1058- location : { offset : node . offset , length : 1 } ,
1090+ location : { offset : node . offset , length : node . length } ,
10591091 message : l10n . t ( 'Missing property "{0}".' , propertyName )
10601092 } ) ;
10611093 }
0 commit comments