@@ -38,6 +38,11 @@ export function parseAjvError(
3838 let fieldKey : string ;
3939 let message : string ;
4040
41+ const cleanPropertyName = ( name : string ) => {
42+ // remove leading and trailing slashes and replace remaining slashes with dots
43+ return name . replace ( / ^ \/ | \/ $ / g, '' ) . replace ( / \/ / g, '.' ) ;
44+ } ;
45+
4146 // If error is with keyword type, it means that type of input is incorrect
4247 // this can mean that provided value is null
4348 if ( error . keyword === 'type' ) {
@@ -48,20 +53,20 @@ export function parseAjvError(
4853 }
4954 message = m ( 'inputSchema.validation.generic' , { rootName, fieldKey, message : error . message } ) ;
5055 } else if ( error . keyword === 'required' ) {
51- fieldKey = error . params . missingProperty ;
56+ fieldKey = cleanPropertyName ( ` ${ error . instancePath } / ${ error . params . missingProperty } ` ) ;
5257 message = m ( 'inputSchema.validation.required' , { rootName, fieldKey } ) ;
5358 } else if ( error . keyword === 'additionalProperties' ) {
54- fieldKey = error . params . additionalProperty ;
59+ fieldKey = cleanPropertyName ( ` ${ error . instancePath } / ${ error . params . additionalProperty } ` ) ;
5560 message = m ( 'inputSchema.validation.additionalProperty' , { rootName, fieldKey } ) ;
5661 } else if ( error . keyword === 'enum' ) {
57- fieldKey = error . instancePath . split ( '/' ) . pop ( ) ! ;
62+ fieldKey = cleanPropertyName ( error . instancePath ) ;
5863 const errorMessage = `${ error . message } : "${ error . params . allowedValues . join ( '", "' ) } "` ;
5964 message = m ( 'inputSchema.validation.generic' , { rootName, fieldKey, message : errorMessage } ) ;
6065 } else if ( error . keyword === 'const' ) {
61- fieldKey = error . instancePath . split ( '/' ) . pop ( ) ! ;
66+ fieldKey = cleanPropertyName ( error . instancePath ) ;
6267 message = m ( 'inputSchema.validation.generic' , { rootName, fieldKey, message : error . message } ) ;
6368 } else {
64- fieldKey = error . instancePath . split ( '/' ) . pop ( ) ! ;
69+ fieldKey = cleanPropertyName ( error . instancePath ) ;
6570 message = m ( 'inputSchema.validation.generic' , { rootName, fieldKey, message : error . message } ) ;
6671 }
6772
@@ -93,10 +98,19 @@ function validateBasicStructure(validator: Ajv, obj: Record<string, unknown>): a
9398/**
9499 * Validates particular field against it's schema.
95100 */
96- function validateField ( validator : Ajv , fieldSchema : Record < string , unknown > , fieldKey : string ) : asserts fieldSchema is FieldDefinition {
101+ function validateField ( validator : Ajv , fieldSchema : Record < string , unknown > , fieldKey : string , subField = false ) : asserts fieldSchema is FieldDefinition {
97102 const matchingDefinitions = Object
98103 . values < any > ( definitions ) // cast as any, as the code in first branch seems to be invalid
99104 . filter ( ( definition ) => {
105+ if ( ! subField && definition . title . startsWith ( 'Sub-schema' ) ) {
106+ // This is a sub-schema definition, so we skip it.
107+ return false ;
108+ }
109+ if ( subField && ! definition . title . startsWith ( 'Sub-schema' ) ) {
110+ // This is a normal definition, so we skip it.
111+ return false ;
112+ }
113+
100114 return definition . properties . type . enum
101115 // This is a normal case where fieldSchema.type can be only one possible value matching definition.properties.type.enum.0
102116 ? definition . properties . type . enum [ 0 ] === fieldSchema . type
@@ -110,9 +124,18 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fie
110124 throw new Error ( `Input schema is not valid (${ errorMessage } )` ) ;
111125 }
112126
127+ // When validating against schema of one definition, the definition can reference other definitions.
128+ // So we need to add all of them to the schema.
129+ function enhanceDefinition ( definition : any ) {
130+ return {
131+ ...definition ,
132+ definitions,
133+ } ;
134+ }
135+
113136 // If there is only one matching then we are done and simply compare it.
114137 if ( matchingDefinitions . length === 1 ) {
115- validateAgainstSchemaOrThrow ( validator , fieldSchema , matchingDefinitions [ 0 ] , `schema.properties.${ fieldKey } ` ) ;
138+ validateAgainstSchemaOrThrow ( validator , fieldSchema , enhanceDefinition ( matchingDefinitions [ 0 ] ) , `schema.properties.${ fieldKey } ` ) ;
116139 return ;
117140 }
118141
@@ -121,30 +144,41 @@ function validateField(validator: Ajv, fieldSchema: Record<string, unknown>, fie
121144 if ( ( fieldSchema as StringFieldDefinition ) . enum ) {
122145 const definition = matchingDefinitions . filter ( ( item ) => ! ! item . properties . enum ) . pop ( ) ;
123146 if ( ! definition ) throw new Error ( 'Input schema validation failed to find "enum property" definition' ) ;
124- validateAgainstSchemaOrThrow ( validator , fieldSchema , definition , `schema.properties.${ fieldKey } .enum` ) ;
147+ validateAgainstSchemaOrThrow ( validator , fieldSchema , enhanceDefinition ( definition ) , `schema.properties.${ fieldKey } .enum` ) ;
125148 return ;
126149 }
127150 // If the definition contains "resourceType" property then it's resource type.
128151 if ( ( fieldSchema as CommonResourceFieldDefinition < unknown > ) . resourceType ) {
129152 const definition = matchingDefinitions . filter ( ( item ) => ! ! item . properties . resourceType ) . pop ( ) ;
130153 if ( ! definition ) throw new Error ( 'Input schema validation failed to find "resource property" definition' ) ;
131- validateAgainstSchemaOrThrow ( validator , fieldSchema , definition , `schema.properties.${ fieldKey } ` ) ;
154+ validateAgainstSchemaOrThrow ( validator , fieldSchema , enhanceDefinition ( definition ) , `schema.properties.${ fieldKey } ` ) ;
132155 return ;
133156 }
134157 // Otherwise we use the other definition.
135158 const definition = matchingDefinitions . filter ( ( item ) => ! item . properties . enum && ! item . properties . resourceType ) . pop ( ) ;
136159 if ( ! definition ) throw new Error ( 'Input schema validation failed to find other than "enum property" definition' ) ;
137160
138- validateAgainstSchemaOrThrow ( validator , fieldSchema , definition , `schema.properties.${ fieldKey } ` ) ;
161+ validateAgainstSchemaOrThrow ( validator , fieldSchema , enhanceDefinition ( definition ) , `schema.properties.${ fieldKey } ` ) ;
162+ }
163+
164+ function validateSubFields ( validator : Ajv , fieldSchema : InputSchemaBaseChecked , fieldKey : string ) {
165+ Object . entries ( fieldSchema . properties ) . forEach ( ( [ subFieldKey , subFieldSchema ] ) => (
166+ validateField ( validator , subFieldSchema , `${ fieldKey } .${ subFieldKey } ` , true ) ) ,
167+ ) ;
139168}
140169
141170/**
142171 * Validates all properties in the input schema
143172 */
144173function validateProperties ( inputSchema : InputSchemaBaseChecked , validator : Ajv ) : asserts inputSchema is InputSchema {
145- Object . entries ( inputSchema . properties ) . forEach ( ( [ fieldKey , fieldSchema ] ) => (
146- validateField ( validator , fieldSchema , fieldKey ) ) ,
147- ) ;
174+ Object . entries ( inputSchema . properties ) . forEach ( ( [ fieldKey , fieldSchema ] ) => {
175+ // The sub-properties has to be validated first, so we got more relevant error messages.
176+ if ( ( fieldSchema as any ) . properties ) {
177+ // If the field has sub-fields, we need to validate them as well.
178+ validateSubFields ( validator , fieldSchema as any as InputSchemaBaseChecked , fieldKey ) ;
179+ }
180+ validateField ( validator , fieldSchema , fieldKey ) ;
181+ } ) ;
148182}
149183
150184/**
0 commit comments