Skip to content

Commit

Permalink
feat: provide current values as context to yup validation by default (#…
Browse files Browse the repository at this point in the history
…3786)

Yup by default only allows for cross-field validation within the
same field object. This is not that useful in most scenarios because
a sufficiently-complex form will have several `yup.object()` in the
schema.

```ts
const deepNestedSchema = Yup.object({
  object: Yup.object({
    nestedField: Yup.number().required(),
  }),
  object2: Yup.object({
    // this doesn't work because `object.nestedField` is outside of `object2`
    nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('object.nestedField')),
  }),
});
```

However, Yup offers something called `context` which can operate across
the entire schema when using a $ prefix:

```ts
const deepNestedSchema = Yup.object({
  object: Yup.object({
    nestedField: Yup.number().required(),
  }),
  object2: Yup.object({
    // this works because of the "context" feature, enabled by $ prefix
    nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('$object.nestedField')),
  }),
});
```

With this change, you may now validate against any field in the entire schema,
regardless of position when using the $ prefix.
  • Loading branch information
quantizor authored May 26, 2023
1 parent d3348d2 commit 39a7bf7
Show file tree
Hide file tree
Showing 3 changed files with 76 additions and 13 deletions.
38 changes: 38 additions & 0 deletions .changeset/lemon-crabs-explain.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
'formik': minor
---

Yup by default only allows for cross-field validation within the
same field object. This is not that useful in most scenarios because
a sufficiently-complex form will have several `yup.object()` in the
schema.

```ts
const deepNestedSchema = Yup.object({
object: Yup.object({
nestedField: Yup.number().required(),
}),
object2: Yup.object({
// this doesn't work because `object.nestedField` is outside of `object2`
nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('object.nestedField')),
}),
});
```

However, Yup offers something called `context` which can operate across
the entire schema when using a \$ prefix:

```ts
const deepNestedSchema = Yup.object({
object: Yup.object({
nestedField: Yup.number().required(),
}),
object2: Yup.object({
// this works because of the "context" feature, enabled by $ prefix
nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('$object.nestedField')),
}),
});
```

With this change, you may now validate against any field in the entire schema,
regardless of position when using the \$ prefix.
9 changes: 5 additions & 4 deletions packages/formik/src/Formik.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1066,12 +1066,13 @@ export function validateYupSchema<T extends FormikValues>(
values: T,
schema: any,
sync: boolean = false,
context: any = {}
context?: any
): Promise<Partial<T>> {
const validateData: FormikValues = prepareDataForValidation(values);
return schema[sync ? 'validateSync' : 'validate'](validateData, {
const normalizedValues: FormikValues = prepareDataForValidation(values);

return schema[sync ? 'validateSync' : 'validate'](normalizedValues, {
abortEarly: false,
context: context,
context: context || normalizedValues,
});
}

Expand Down
42 changes: 33 additions & 9 deletions packages/formik/test/yupHelpers.test.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import * as Yup from 'yup';
import { validateYupSchema, yupToFormErrors } from '../src';

const Yup = require('yup');
const schema = Yup.object().shape({
name: Yup.string('Name must be a string').required('required'),
field: Yup.string('Field must be a string'),
name: Yup.string().required('required'),
field: Yup.string(),
});

const nestedSchema = Yup.object().shape({
object: Yup.object().shape({
nestedField: Yup.string('Field must be a string'),
nestedArray: Yup.array().of(
Yup.string('Field must be a string').nullable()
),
nestedField: Yup.string(),
nestedArray: Yup.array().of(Yup.string().nullable(true)),
}),
});

const deepNestedSchema = Yup.object({
object: Yup.object({
nestedField: Yup.number().required(),
}),
object2: Yup.object({
nestedFieldWithRef: Yup.number().min(0).max(Yup.ref('$object.nestedField')),
}),
});

Expand All @@ -32,8 +40,10 @@ describe('Yup helpers', () => {
try {
await validateYupSchema({}, schema);
} catch (e) {
expect(e.name).toEqual('ValidationError');
expect(e.errors).toEqual(['required']);
const err = e as Yup.ValidationError;

expect(err.name).toEqual('ValidationError');
expect(err.errors).toEqual(['required']);
}
});

Expand Down Expand Up @@ -85,5 +95,19 @@ describe('Yup helpers', () => {
throw e;
}
});

it('should provide current values as context to enable deep object field validation', async () => {
try {
await validateYupSchema(
{ object: { nestedField: 23 }, object2: { nestedFieldWithRef: 24 } },
deepNestedSchema
);
} catch (e) {
expect((e as Yup.ValidationError).name).toEqual('ValidationError');
expect((e as Yup.ValidationError).errors).toEqual([
'object2.nestedFieldWithRef must be less than or equal to 23',
]);
}
});
});
});

1 comment on commit 39a7bf7

@vercel
Copy link

@vercel vercel bot commented on 39a7bf7 May 26, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

formik-docs – ./website

formik-docs-jared.vercel.app
formik-docs.vercel.app
www.formik.org
formik-docs-git-master-jared.vercel.app
formik.org

Please sign in to comment.