From 0323dd6c6abb64cff45a7bd9cbe648f6bd922dba Mon Sep 17 00:00:00 2001 From: Capelo Date: Wed, 23 Jul 2025 10:32:13 +0100 Subject: [PATCH 1/5] fix: only add options to field if it's not the root --- src/field/schema.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/field/schema.ts b/src/field/schema.ts index 907fb488e..76ca0e4af 100644 --- a/src/field/schema.ts +++ b/src/field/schema.ts @@ -420,7 +420,10 @@ export function buildFieldSchema({ }) } - addOptions(field, schema) + if (name !== 'root') { + addOptions(field, schema) + } + addFields(field, schema, originalSchema) return field From fcf861f7a1fc0f6e5f4b14fb13a8805010f5c9ef Mon Sep 17 00:00:00 2001 From: Capelo Date: Wed, 23 Jul 2025 10:33:16 +0100 Subject: [PATCH 2/5] feat: improve error messages for anyOf conditions --- src/errors/messages.ts | 2 -- src/validation/composition.ts | 27 ++++++++++++++++----------- test/validation/composition.test.ts | 7 ++++--- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/errors/messages.ts b/src/errors/messages.ts index 8e18167a5..feef5844a 100644 --- a/src/errors/messages.ts +++ b/src/errors/messages.ts @@ -43,8 +43,6 @@ export function getErrorMessage( case 'enum': return `The option "${valueToString(value)}" is not valid.` // Schema composition - case 'anyOf': - return `The option "${valueToString(value)}" is not valid.` case 'oneOf': return `The option "${valueToString(value)}" is not valid.` case 'not': diff --git a/src/validation/composition.ts b/src/validation/composition.ts index 32d139954..cb818c3d7 100644 --- a/src/validation/composition.ts +++ b/src/validation/composition.ts @@ -75,21 +75,26 @@ export function validateAnyOf( return [] } + const errorGroups: ValidationError[][] = [] + + // Check how many rules inside the anyOf array are met for (const subSchema of schema.anyOf) { - const errors = validateSchema(value, subSchema, options, path, jsonLogicContext) - if (errors.length === 0) { - return [] + const schemaErrors = validateSchema(value, subSchema, options, path, jsonLogicContext) + // If the schema is not valid, add the errors to the errorGroups array + if (schemaErrors.length !== 0) { + errorGroups.push(schemaErrors) } } - return [ - { - path, - validation: 'anyOf', - schema, - value, - }, - ] + // If the number of failed rules is less than the number of rules, it means that the + // "any of" condition is met, so we return an empty array. Otherwise, we return the flattened errors. + const anyConditionMet = errorGroups.length < schema.anyOf.length + if (anyConditionMet) { + return [] + } + else { + return errorGroups.flat() + } } /** diff --git a/test/validation/composition.test.ts b/test/validation/composition.test.ts index 062b57605..11f99330c 100644 --- a/test/validation/composition.test.ts +++ b/test/validation/composition.test.ts @@ -148,8 +148,9 @@ describe('schema composition validators', () => { it('should fail when value matches no schema', () => { const value = 'too long' const errors = validateSchema(value, schema) - expect(errors).toHaveLength(1) - expect(errors[0].validation).toBe('anyOf') + expect(errors).toHaveLength(2) + expect(errors[0].validation).toBe('maxLength') + expect(errors[1].validation).toBe('type') }) }) @@ -225,7 +226,7 @@ describe('schema composition validators', () => { it('should fail for non-null values', () => { const errors = validateSchema(123, schema) expect(errors).toHaveLength(1) - expect(errors[0].validation).toBe('anyOf') + expect(errors[0].validation).toBe('type') }) }) }) From 96781a79337cecf67d8042b2545bf17d2abf30fd Mon Sep 17 00:00:00 2001 From: Capelo Date: Wed, 23 Jul 2025 14:57:17 +0100 Subject: [PATCH 3/5] chore: fix type --- src/errors/index.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/errors/index.ts b/src/errors/index.ts index dccd3cab5..15afe7d27 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -18,7 +18,6 @@ export type SchemaValidationErrorType = * Schema composition keywords (allOf, anyOf, oneOf, not) * These keywords apply subschemas in a logical manner according to JSON Schema spec */ - | 'anyOf' | 'oneOf' | 'not' /** From ad16532fc649647949824d623b6c319815933400 Mon Sep 17 00:00:00 2001 From: Capelo Date: Wed, 23 Jul 2025 17:01:51 +0100 Subject: [PATCH 4/5] chore: refactor --- src/errors/index.ts | 1 + src/errors/messages.ts | 2 ++ src/validation/composition.ts | 31 +++++++++++++++++++++++---- test/errors/messages.test.ts | 40 +++++++++++++++++++++++++++++++++++ 4 files changed, 70 insertions(+), 4 deletions(-) diff --git a/src/errors/index.ts b/src/errors/index.ts index 15afe7d27..539e9063e 100644 --- a/src/errors/index.ts +++ b/src/errors/index.ts @@ -20,6 +20,7 @@ export type SchemaValidationErrorType = */ | 'oneOf' | 'not' + | 'anyOf' /** * String validation keywords */ diff --git a/src/errors/messages.ts b/src/errors/messages.ts index feef5844a..7f796658e 100644 --- a/src/errors/messages.ts +++ b/src/errors/messages.ts @@ -45,6 +45,8 @@ export function getErrorMessage( // Schema composition case 'oneOf': return `The option "${valueToString(value)}" is not valid.` + case 'anyOf': + return `The option "${valueToString(value)}" is not valid.` case 'not': return 'The value must not satisfy the provided schema' // String validation diff --git a/src/validation/composition.ts b/src/validation/composition.ts index cb818c3d7..c2eb33bf5 100644 --- a/src/validation/composition.ts +++ b/src/validation/composition.ts @@ -75,9 +75,32 @@ export function validateAnyOf( return [] } + // If the path is not empty, we are validating a nested schema (property). + // In this case, we need to check if any of the sub-schemas are valid. If not, we indicate + // the field is invalid with a generic (anyOf) error. + if (path.length !== 0) { + for (const subSchema of schema.anyOf) { + const errors = validateSchema(value, subSchema, options, path, jsonLogicContext) + if (errors.length === 0) { + return [] + } + } + + return [ + { + path, + validation: 'anyOf', + schema, + value, + }, + ] + } + const errorGroups: ValidationError[][] = [] - // Check how many rules inside the anyOf array are met + // If the path is empty, we are validating the root schema. + // If the number of failed rules is less than the number of rules, it means that the + // "any of" condition is met, so we return an empty array. Otherwise, we return the flattened errors. for (const subSchema of schema.anyOf) { const schemaErrors = validateSchema(value, subSchema, options, path, jsonLogicContext) // If the schema is not valid, add the errors to the errorGroups array @@ -86,14 +109,14 @@ export function validateAnyOf( } } - // If the number of failed rules is less than the number of rules, it means that the - // "any of" condition is met, so we return an empty array. Otherwise, we return the flattened errors. const anyConditionMet = errorGroups.length < schema.anyOf.length if (anyConditionMet) { return [] } else { - return errorGroups.flat() + // Reversing the errors to show the first error that occurred (in the addErrorMessages function, + // the last error is usually the one being displayed) + return errorGroups.flat().reverse() } } diff --git a/test/errors/messages.test.ts b/test/errors/messages.test.ts index f1e2ed3e2..9fe4c4439 100644 --- a/test/errors/messages.test.ts +++ b/test/errors/messages.test.ts @@ -514,6 +514,46 @@ describe('validation error messages', () => { }) }) + it('shows field validation error messages for nested schemas in a root anyOf', () => { + const schema: JsfObjectSchema = { + type: 'object', + properties: { + field_a: { + type: 'string', + }, + field_b: { + type: 'string', + }, + }, + anyOf: [ + { required: ['field_a'] }, + { required: ['field_b'] }, + ], + } + const form = createHeadlessForm(schema) + + // Check that we get an error for both fields if none is provided + let result = form.handleValidation({ }) + + expect(result.formErrors).toMatchObject({ + field_a: 'Required field', + field_b: 'Required field', + }) + + // Check that we get don't get an error if one of the fields is provided + result = form.handleValidation({ + field_a: '123456', + }) + + expect(result.formErrors).toBeUndefined() + + result = form.handleValidation({ + field_b: '123456', + }) + + expect(result.formErrors).toBeUndefined() + }) + it('shows oneOf validation error messages', () => { const schema: JsfObjectSchema = { type: 'object', From f10c585d215e8c509a3e899b912a398090ec1975 Mon Sep 17 00:00:00 2001 From: Capelo Date: Wed, 23 Jul 2025 18:04:56 +0100 Subject: [PATCH 5/5] fix: fix test --- test/validation/composition.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/validation/composition.test.ts b/test/validation/composition.test.ts index 11f99330c..71f7f3c71 100644 --- a/test/validation/composition.test.ts +++ b/test/validation/composition.test.ts @@ -149,8 +149,8 @@ describe('schema composition validators', () => { const value = 'too long' const errors = validateSchema(value, schema) expect(errors).toHaveLength(2) - expect(errors[0].validation).toBe('maxLength') - expect(errors[1].validation).toBe('type') + expect(errors[0].validation).toBe('type') + expect(errors[1].validation).toBe('maxLength') }) })