@@ -29,6 +29,12 @@ import { parse } from 'yaml';
2929import * as Json from 'jsonc-parser' ;
3030import { getSchemaTitle } from '../utils/schemaUtils' ;
3131
32+ import * as Draft04 from '@hyperjump/json-schema/draft-04' ;
33+ import * as Draft07 from '@hyperjump/json-schema/draft-07' ;
34+ import * as Draft201909 from '@hyperjump/json-schema/draft-2019-09' ;
35+ import * as Draft202012 from '@hyperjump/json-schema/draft-2020-12' ;
36+
37+ type SupportedSchemaVersions = '2020-12' | '2019-09' | 'draft-07' | 'draft-04' ;
3238export declare type CustomSchemaProvider = ( uri : string ) => Promise < string | string [ ] > ;
3339
3440export enum MODIFICATION_ACTIONS {
@@ -154,12 +160,34 @@ export class YAMLSchemaService extends JSONSchemaService {
154160 let schema : JSONSchema = schemaToResolve . schema ;
155161 const contextService = this . contextService ;
156162
157- // Basic schema validation - check if schema is a valid object
158163 if ( typeof schema !== 'object' || schema === null || Array . isArray ( schema ) ) {
159164 const invalidSchemaType = Array . isArray ( schema ) ? 'array' : typeof schema ;
160165 resolveErrors . push (
161166 `Schema '${ getSchemaTitle ( schemaToResolve . schema , schemaURL ) } ' is not valid:\nWrong schema: "${ invalidSchemaType } ", it MUST be an Object or Boolean`
162167 ) ;
168+ } else {
169+ try {
170+ const schemaVersion = this . detectSchemaVersion ( schema ) ;
171+ const validator = this . getValidatorForVersion ( schemaVersion ) ;
172+ const metaSchemaUrl = this . getSchemaMetaSchema ( schemaVersion ) ;
173+
174+ // Validate the schema against its meta-schema using the URL directly
175+ const result = await validator . validate ( metaSchemaUrl , schema , 'BASIC' ) ;
176+ if ( ! result . valid && result . errors ) {
177+ const errs : string [ ] = [ ] ;
178+ for ( const error of result . errors ) {
179+ if ( error . instanceLocation && error . keyword ) {
180+ errs . push ( `${ error . instanceLocation } : ${ this . extractKeywordName ( error . keyword ) } constraint violation` ) ;
181+ }
182+ }
183+ if ( errs . length > 0 ) {
184+ resolveErrors . push ( `Schema '${ getSchemaTitle ( schemaToResolve . schema , schemaURL ) } ' is not valid:\n${ errs . join ( '\n' ) } ` ) ;
185+ }
186+ }
187+ } catch ( error ) {
188+ // If meta-schema validation fails, log but don't block schema loading
189+ console . error ( `Failed to validate schema meta-schema: ${ error . message } ` ) ;
190+ }
163191 }
164192
165193 const findSection = ( schema : JSONSchema , path : string ) : JSONSchema => {
@@ -268,16 +296,16 @@ export class YAMLSchemaService extends JSONSchemaService {
268296 const seenRefs = new Set ( ) ;
269297 while ( next . $ref ) {
270298 const ref = decodeURIComponent ( next . $ref ) ;
271- const segments = ref . split ( '#' , 2 ) ;
272299 //return back removed $ref. We lost info about referenced type without it.
300+ const segments = ref . split ( '#' , 2 ) ;
273301 next . _$ref = next . $ref ;
274302 delete next . $ref ;
275303 if ( segments [ 0 ] . length > 0 ) {
276304 openPromises . push ( resolveExternalLink ( next , segments [ 0 ] , segments [ 1 ] , parentSchemaURL , parentSchemaDependencies ) ) ;
277305 return ;
278306 } else {
279307 if ( ! seenRefs . has ( ref ) ) {
280- merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ; // can set next.$ref again, use seenRefs to avoid circle
308+ merge ( next , parentSchema , parentSchemaURL , segments [ 1 ] ) ;
281309 seenRefs . add ( ref ) ;
282310 }
283311 }
@@ -475,7 +503,6 @@ export class YAMLSchemaService extends JSONSchemaService {
475503 if ( prio > highestPrio ) {
476504 highestPrio = prio ;
477505 }
478-
479506 // Build up a mapping of priority to schemas so that we can easily get the highest priority schemas easier
480507 let currPriorityArray = priorityMapping . get ( prio ) ;
481508 if ( currPriorityArray ) {
@@ -620,10 +647,10 @@ export class YAMLSchemaService extends JSONSchemaService {
620647
621648 loadSchema ( schemaUri : string ) : Promise < UnresolvedSchema > {
622649 const requestService = this . requestService ;
650+ // If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
651+ // If the YAML file starts with %YAML 1.x or contains a comment with a number the schema will
652+ // contain a number instead of being undefined, so we need to check for that too.
623653 return super . loadSchema ( schemaUri ) . then ( async ( unresolvedJsonSchema : UnresolvedSchema ) => {
624- // If json-language-server failed to parse the schema, attempt to parse it as YAML instead.
625- // If the YAML file starts with %YAML 1.x or contains a comment with a number the schema will
626- // contain a number instead of being undefined, so we need to check for that too.
627654 if (
628655 unresolvedJsonSchema . errors &&
629656 ( unresolvedJsonSchema . schema === undefined || typeof unresolvedJsonSchema . schema === 'number' )
@@ -656,9 +683,9 @@ export class YAMLSchemaService extends JSONSchemaService {
656683 // eslint-disable-next-line @typescript-eslint/no-explicit-any
657684 ( error : any ) => {
658685 let errorMessage = error . toString ( ) ;
686+ // more concise error message, URL and context are attached by caller anyways
659687 const errorSplit = error . toString ( ) . split ( 'Error: ' ) ;
660688 if ( errorSplit . length > 1 ) {
661- // more concise error message, URL and context are attached by caller anyways
662689 errorMessage = errorSplit [ 1 ] ;
663690 }
664691 return new UnresolvedSchema ( < JSONSchema > { } , [ errorMessage ] ) ;
@@ -725,6 +752,79 @@ export class YAMLSchemaService extends JSONSchemaService {
725752 onResourceChange ( uri : string ) : boolean {
726753 return super . onResourceChange ( uri ) ;
727754 }
755+
756+ /**
757+ * Detect the JSON Schema version from the $schema property
758+ */
759+ private detectSchemaVersion ( schema : JSONSchema ) : SupportedSchemaVersions {
760+ const schemaProperty = schema . $schema ;
761+ if ( typeof schemaProperty === 'string' ) {
762+ if ( schemaProperty . includes ( '2020-12' ) ) {
763+ return '2020-12' ;
764+ } else if ( schemaProperty . includes ( '2019-09' ) ) {
765+ return '2019-09' ;
766+ } else if ( schemaProperty . includes ( 'draft-07' ) || schemaProperty . includes ( 'draft/7' ) ) {
767+ return 'draft-07' ;
768+ } else if ( schemaProperty . includes ( 'draft-04' ) || schemaProperty . includes ( 'draft/4' ) ) {
769+ return 'draft-04' ;
770+ }
771+ }
772+ return 'draft-07' ;
773+ }
774+
775+ /**
776+ * Get the appropriate validator module for a schema version
777+ */
778+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
779+ private getValidatorForVersion ( version : SupportedSchemaVersions ) : any {
780+ switch ( version ) {
781+ case '2020-12' :
782+ return Draft202012 ;
783+ case '2019-09' :
784+ return Draft201909 ;
785+ case 'draft-07' :
786+ return Draft07 ;
787+ case 'draft-04' :
788+ default :
789+ return Draft04 ;
790+ }
791+ }
792+
793+ /**
794+ * Get the correct schema meta URI for a given version
795+ */
796+ private getSchemaMetaSchema ( version : SupportedSchemaVersions ) : string {
797+ switch ( version ) {
798+ case '2020-12' :
799+ return 'https://json-schema.org/draft/2020-12/schema' ;
800+ case '2019-09' :
801+ return 'https://json-schema.org/draft/2019-09/schema' ;
802+ case 'draft-07' :
803+ return 'http://json-schema.org/draft-07/schema' ;
804+ case 'draft-04' :
805+ return 'http://json-schema.org/draft-04/schema' ;
806+ default :
807+ return 'http://json-schema.org/draft-07/schema' ;
808+ }
809+ }
810+
811+ /**
812+ * Extract a human-readable keyword name from a keyword URI
813+ */
814+ private extractKeywordName ( keywordUri : string ) : string {
815+ if ( typeof keywordUri !== 'string' ) {
816+ return 'validation' ;
817+ }
818+
819+ const parts = keywordUri . split ( '/' ) ;
820+ const lastPart = parts [ parts . length - 1 ] ;
821+
822+ if ( lastPart === 'validate' ) {
823+ return 'schema validation' ;
824+ }
825+
826+ return lastPart || 'validation' ;
827+ }
728828}
729829
730830function toDisplayString ( url : string ) : string {
0 commit comments