Skip to content

Commit

Permalink
Merge pull request #2 from Glazy/array-and-object-support
Browse files Browse the repository at this point in the history
Array and object support.
  • Loading branch information
Glazy authored Feb 23, 2025
2 parents 44130f7 + 1588c31 commit 930f7c8
Show file tree
Hide file tree
Showing 5 changed files with 194 additions and 29 deletions.
33 changes: 15 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,37 +3,25 @@
Allows you to easily validate [Formik](https://github.com/jaredpalmer/formik)
forms with the power of [Zod](https://github.com/colinhacks/zod) schemas.

WARNING: As of v2, this package uses native ESM and no longer provides a
CommonJS export. If this is something you need, you should be able to use the
[dynamic import](https://v8.dev/features/dynamic-import) function or use v1 of
this package.
> [!IMPORTANT] WARNING: As of v2.0, this package uses ESM and no longer provides
> a CommonJS export. If this is something you need, you should be able to use
> the [dynamic import](https://v8.dev/features/dynamic-import) function.
## Installation

This package is published both on [NPM](https://www.npmjs.com) and
[JSR](https://jsr.io/).

To install from NPM:

```sh
npm install formik-validator-zod

pnpm add formik-validator-zod

yarn add formik-validator-zod

bun add formik-validator-zod
```

To install from JSR:

```sh
npx jsr add @glazy/formik-validator-zod

yarn dlx jsr add @glazy/formik-validator-zod

bunx jsr add @glazy/formik-validator-zod
```

## Example
## Usage

```jsx
import { Formik } from 'formik'
Expand All @@ -54,3 +42,12 @@ const MyForm = () => {
)
}
```

## Is this library still maintained?

Yes! This library is used in a couple of production codebases that I'm aware of,
including my current employers.

I don't expect the library will need a lot of active maintenance going forwards.
This is due to its limited scope and the fact Formik itsely seems to be
abandoned.
17 changes: 16 additions & 1 deletion lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ZodSchema, ParseParams } from 'zod'
import merge from 'deepmerge'

/**
* Allows you to easily use Zod schemas with the <Formik /> component `validate`
Expand All @@ -16,7 +17,21 @@ export const withZodSchema =
if (result.success) return {}

return result.error.issues.reduce((acc, curr) => {
const key = curr.path.join('.')
if (curr.path.length) {
return merge(
acc,
curr.path.reduceRight(
(errors, pathSegment) => ({
[pathSegment]: !Object.keys(errors).length
? curr.message
: errors,
}),
{}
)
)
}

const key = curr.path[0]
return {
...acc,
[key]: curr.message,
Expand Down
38 changes: 28 additions & 10 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,8 @@
"peerDependencies": {
"formik": "^2.2.9",
"zod": "^3.19.1"
},
"dependencies": {
"deepmerge": "^4.3.1"
}
}
132 changes: 132 additions & 0 deletions test/index.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { describe, expect, it } from 'vitest'
import { z } from 'zod'
import { getIn } from 'formik'
import { withZodSchema } from '../lib/index'

const testSchema = z.object({
Expand Down Expand Up @@ -36,4 +37,135 @@ describe('withZodSchema', () => {
favouriteValue: 'Invalid input',
})
})

describe('nested objects', () => {
const schema = z.object({
user: z.object({
name: z.string().min(2),
email: z.string().email(),
}),
})

it('returns no errors for valid data', () => {
const result = withZodSchema(schema)({
user: {
name: 'Luke',
email: '[email protected]',
},
})

expect(Object.keys(result).length).toEqual(0)
})

it('returns field error if no children have errors', () => {
// @ts-ignore Type incorrect for usage in test case.
const result = withZodSchema(schema)({})

expect(getIn(result, 'user')).toEqual('Required')
})

it('returns object errors correctly', () => {
const result = withZodSchema(schema)({
user: {
name: 'X',
email: 'invalid-email',
},
})

expect(getIn(result, 'user.name')).toEqual(
'String must contain at least 2 character(s)'
)
expect(getIn(result, 'user.email')).toEqual('Invalid email')
})
})

describe('simple arrays', () => {
const schema = z.object({
favouriteColours: z.string().array().min(3),
})

it('returns field issue if children are valid', () => {
const result = withZodSchema(schema)({
favouriteColours: ['Yellow', 'Red'],
})

expect(getIn(result, 'favouriteColours')).toEqual(
'Array must contain at least 3 element(s)'
)
})

it('returns no errors for valid data', () => {
const result = withZodSchema(schema)({
favouriteColours: ['Yellow', 'Blue', 'Purple', 'Red'],
})

expect(Object.keys(result).length).toEqual(0)
})

it('returns error in array correctly', () => {
const result = withZodSchema(schema)({
// @ts-ignore Incorrect type to facilitate test case.
favouriteColours: ['Yellow', 'Red', 42],
})

expect(getIn(result, 'favouriteColours.1')).toBeUndefined()
expect(getIn(result, 'favouriteColours.2')).toEqual(
'Expected string, received number'
)
})
})

describe('array of objects', () => {
const schema = z.object({
footballTeams: z
.object({
name: z.string().endsWith('FC'),
manager: z.string().min(2),
})
.array()
.min(2),
})

it('returns no errors for correct data', () => {
const result = withZodSchema(schema)({
footballTeams: [
{ name: 'Green FC', manager: 'Mr Green' },
{ name: 'Red FC', manager: 'Mr Red' },
],
})

expect(Object.keys(result).length).toEqual(0)
})

it('returns field error if no children have errors', () => {
const result = withZodSchema(schema)({
footballTeams: [{ name: 'Green FC', manager: 'Mr Green' }],
})

expect(getIn(result, 'footballTeams')).toEqual(
'Array must contain at least 2 element(s)'
)
})

it('returns errors correctly', () => {
const result = withZodSchema(schema)({
footballTeams: [
{ name: 'Green Athletic', manager: 'X' },
{ name: 'Red FC', manager: 'X' },
],
})

expect(getIn(result, 'footballTeams[0].name')).toEqual(
'Invalid input: must end with "FC"'
)
expect(getIn(result, 'footballTeams[0].manager')).toEqual(
'String must contain at least 2 character(s)'
)

expect(getIn(result, 'footballTeams[1].name')).toBeUndefined()
expect(getIn(result, 'footballTeams[1].manager')).toEqual(
'String must contain at least 2 character(s)'
)
})
})
})

0 comments on commit 930f7c8

Please sign in to comment.