11/* eslint-disable @typescript-eslint/no-non-null-assertion */
22import { ParseConfigurationResult } from './schema.js'
3+ import { randomUUID } from './crypto.js'
34import { getPathValue } from '../common/object.js'
45import { capitalize } from '../common/string.js'
56import { Ajv , ErrorObject , SchemaObject , ValidateFunction } from 'ajv'
67import $RefParser from '@apidevtools/json-schema-ref-parser'
8+ import cloneDeep from 'lodash/cloneDeep.js'
9+
10+ export type HandleInvalidAdditionalProperties = 'strip' | 'fail'
711
812type AjvError = ErrorObject < string , { [ key : string ] : unknown } >
913
@@ -22,6 +26,29 @@ export async function normaliseJsonSchema(schema: string): Promise<SchemaObject>
2226 return parsedSchema
2327}
2428
29+ function createAjvValidator (
30+ handleInvalidAdditionalProperties : HandleInvalidAdditionalProperties ,
31+ schema : SchemaObject ,
32+ ) {
33+ // allowUnionTypes: Allows types like `type: ["string", "number"]`
34+ // removeAdditional: Removes extraneous properties from the subject if `additionalProperties: false` is specified
35+ let removeAdditional
36+ switch ( handleInvalidAdditionalProperties ) {
37+ case 'strip' :
38+ removeAdditional = true
39+ break
40+ case 'fail' :
41+ removeAdditional = undefined
42+ break
43+ }
44+ const ajv = new Ajv ( { allowUnionTypes : true , removeAdditional} )
45+ ajv . addKeyword ( 'x-taplo' )
46+
47+ const validator = ajv . compile ( schema )
48+
49+ return validator
50+ }
51+
2552const validatorsCache = new Map < string , ValidateFunction > ( )
2653
2754/**
@@ -31,27 +58,29 @@ const validatorsCache = new Map<string, ValidateFunction>()
3158 *
3259 * @param subject - The object to validate.
3360 * @param schema - The JSON schema to validate against.
61+ * @param handleInvalidAdditionalProperties - Whether to strip or fail on invalid additional properties.
3462 * @param identifier - The identifier of the schema being validated, used to cache the validator.
3563 * @returns The result of the validation. If the state is 'error', the errors will be in a zod-like format.
3664 */
3765export function jsonSchemaValidate (
3866 subject : object ,
3967 schema : SchemaObject ,
40- identifier : string ,
68+ handleInvalidAdditionalProperties : HandleInvalidAdditionalProperties ,
69+ identifier ?: string ,
4170) : ParseConfigurationResult < unknown > & { rawErrors ?: AjvError [ ] } {
42- const ajv = new Ajv ( { allowUnionTypes : true } )
71+ const subjectToModify = cloneDeep ( subject )
4372
44- ajv . addKeyword ( 'x-taplo' )
73+ const cacheKey = identifier ?? randomUUID ( )
4574
46- const validator = validatorsCache . get ( identifier ) ?? ajv . compile ( schema )
47- validatorsCache . set ( identifier , validator )
75+ const validator = validatorsCache . get ( cacheKey ) ?? createAjvValidator ( handleInvalidAdditionalProperties , schema )
76+ validatorsCache . set ( cacheKey , validator )
4877
49- validator ( subject )
78+ validator ( subjectToModify )
5079
5180 // Errors from the contract are post-processed to be more zod-like and to deal with unions better
5281 let jsonSchemaErrors
5382 if ( validator . errors && validator . errors . length > 0 ) {
54- jsonSchemaErrors = convertJsonSchemaErrors ( validator . errors , subject , schema )
83+ jsonSchemaErrors = convertJsonSchemaErrors ( validator . errors , subjectToModify , schema )
5584 return {
5685 state : 'error' ,
5786 data : undefined ,
@@ -61,7 +90,7 @@ export function jsonSchemaValidate(
6190 }
6291 return {
6392 state : 'ok' ,
64- data : subject ,
93+ data : subjectToModify ,
6594 errors : undefined ,
6695 rawErrors : undefined ,
6796 }
@@ -162,8 +191,6 @@ function convertJsonSchemaErrors(rawErrors: AjvError[], subject: object, schema:
162191 * @returns A simplified list of errors.
163192 */
164193function simplifyUnionErrors ( rawErrors : AjvError [ ] , subject : object , schema : SchemaObject ) : AjvError [ ] {
165- const ajv = new Ajv ( { allowUnionTypes : true } )
166- ajv . addKeyword ( 'x-taplo' )
167194 let errors = rawErrors
168195
169196 const resolvedUnionErrors = new Set ( )
@@ -191,7 +218,7 @@ function simplifyUnionErrors(rawErrors: AjvError[], subject: object, schema: Sch
191218 // we know that none of the union schemas are correct, but for each of them we can measure how wrong they are
192219 const correctValuesAndErrors = unionSchemas
193220 . map ( ( candidateSchemaFromUnion : SchemaObject ) => {
194- const candidateSchemaValidator = ajv . compile ( candidateSchemaFromUnion )
221+ const candidateSchemaValidator = createAjvValidator ( 'fail' , candidateSchemaFromUnion )
195222 candidateSchemaValidator ( subjectValue )
196223
197224 let score = 0
@@ -202,7 +229,7 @@ function simplifyUnionErrors(rawErrors: AjvError[], subject: object, schema: Sch
202229 const subSchema = candidateSchemaFromUnion . properties [ propertyName ] as SchemaObject
203230 const subjectValueSlice = getPathValue ( subjectValue , propertyName )
204231
205- const subValidator = ajv . compile ( subSchema )
232+ const subValidator = createAjvValidator ( 'fail' , subSchema )
206233 if ( subValidator ( subjectValueSlice ) ) {
207234 return acc + 1
208235 }
0 commit comments