Skip to content

Instantly share code, notes, and snippets.

@colinhacks
Last active April 30, 2024 21:42
Show Gist options
  • Save colinhacks/d35825e505e635df27cc950776c5500b to your computer and use it in GitHub Desktop.
Save colinhacks/d35825e505e635df27cc950776c5500b to your computer and use it in GitHub Desktop.

Adapted from this recommendation by @jandockx

Cyclical objects

Despite supporting recursive schemas, passing cyclical data into Zod will cause an infinite loop in some cases.

You can protect against cyclical objects starting an infinite loop (at a performance cost) with the following approach (using the above jsonSchema as an example):

function isCircular(v: unknown, visited?: Set<unknown>): boolean {
  if (v === null || typeof v !== 'object') {
    return false;
  }

  if (visited?.has(v)) {
    return true;
  }

  const actualVisited = visited ?? new Set<unknown>();
  actualVisited.add(v);

  if (Array.isArray(v)) {
    return v.some(av => isCircular(av, actualVisited));
  }

  return Object.values(v).some(ov => isCircular(ov, actualVisited));
}

const NotCircular = z.unknown().superRefine((val, ctx) => {
  if (isCircular(val)) {
    ctx.addIssue({
      code: z.ZodIssueCode.custom,
      message: 'values cannot be circular data structures',
      fatal: true
    });

    return z.NEVER;
  }
})

const acircularJSONSchema = NotCircular.pipe(jsonSchema);

acircularJSONSchema.parse(data);

When NotCircular fails, pipe will not pass the value into the next schema for evaluation, preventing the infinite loop.

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