Skip to content

Instantly share code, notes, and snippets.

@flofehrenbacher
Created February 23, 2022 11:09
Show Gist options
  • Save flofehrenbacher/71f93f7a423cfc11e1dd018325c241ec to your computer and use it in GitHub Desktop.
Save flofehrenbacher/71f93f7a423cfc11e1dd018325c241ec to your computer and use it in GitHub Desktop.
import { z, ZodTypeAny, ZodUnion } from 'zod'
/**
* Zod helper for parsing arrays and ignore items not specified in the schema
*
* @param zodUnion - union of known types
*
* @example
* const binaryArraySchema = arrayIgnoreUnknown(z.union([z.literal('0'), z.literal('1')]))
* type BinaryArray = z.TypeOf<typeof binaryArraySchema>
*
* const binaryArray: BinaryArray = binaryArraySchema.parse(['0', '1', '2', '0'])
* console.log(binaryArray) // ['0', '1', '0']
*/
export function zodArrayIgnoreUnknown<T extends [ZodTypeAny, ...ZodTypeAny[]]>(
zodUnion: ZodUnion<T>,
) {
const isKnownItem = (item: unknown) => zodUnion.safeParse(item).success
return z.preprocess((val) => toSafeArray(val).filter(isKnownItem), z.array(zodUnion))
}
function toSafeArray(item: unknown): Array<unknown> {
if (isArray(item)) {
return item
}
return [item]
}
function isArray<T>(item: unknown): item is Array<T> {
return Array.isArray(item)
}
@midzdotdev
Copy link

midzdotdev commented Nov 12, 2022

I think I've managed to safely simplify this. The one difference is that mine will fail to parse when the input is not an array. Although you can easily union this parser with z.any() or z.unknown() to reflect your parser's behaviour.

/** Parses an array and ignores mismatching elements. */
export const zNonStrictArray = <T>(element: z.ZodType<T>) =>
  z
    .array(z.unknown())
    .transform((xs) =>
      xs.filter((item): item is T => element.safeParse(item).success)
    );

EDIT: actually mine removes the zod array refinements like .min() so I'm sticking with yours.

@tintin10q
Copy link

@flofehrenbacher Why do you have to give at least two inputs to the union?

@flofehrenbacher
Copy link
Author

@tintin10q If I remember correctly the ZodUnion requires at least two inputs. Because of that I used the same constraint

@vimutti77
Copy link

vimutti77 commented Jun 21, 2023

Thanks, the preprocess solution is working with recursive schema.

function makeFilteredArraySchema<T extends ZodSchema>(schema: T) {
  return z.preprocess((val) => {
    const array = Array.isArray(val) ? val : [val]
    return array.filter((item: unknown) => schema.safeParse(item).success)
  }, z.array(schema))
}

Usage

const baseCategorySchema = z.object({
  name: z.string(),
})

type Category = z.infer<typeof baseCategorySchema> & {
  subcategories: Category[]
}

const categorySchema: z.ZodType<Category, z.ZodTypeDef, unknown> = baseCategorySchema.extend({
  subcategories: z.lazy(() => makeFilteredArraySchema(categorySchema)),
})

categorySchema.parse({
  name: 'People',
  subcategories: [
    {
      name: 'Politicians',
      subcategories: [
        {
          name: 'Presidents',
          subcategories: [],
        },
        {
          name: 123,
          subcategories: [],
        },
      ],
    },
    {
      name: 456,
      subcategories: [],
    },
  ],
})

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment