Skip to content

Instantly share code, notes, and snippets.

@colinhacks
Last active October 30, 2025 20:02
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.

Revisions

  1. colinhacks revised this gist Apr 30, 2024. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion cyclical_data.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    > Adapted from [this recommendation](https://github.com/colinhacks/zod/pull/3447) by @jandockx
    > Adapted from [this recommendation](https://github.com/colinhacks/zod/pull/3447) by [@jandockx](https://github.com/jandockx)
    ### Cyclical objects

  2. colinhacks revised this gist Apr 30, 2024. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions cyclical_data.md
    Original file line number Diff line number Diff line change
    @@ -1,3 +1,5 @@
    > Adapted from [this recommendation](https://github.com/colinhacks/zod/pull/3447) by @jandockx
    ### Cyclical objects

    Despite supporting recursive schemas, passing cyclical data into Zod will cause an infinite loop in some cases.
  3. colinhacks created this gist Apr 30, 2024.
    46 changes: 46 additions & 0 deletions cyclical_data.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,46 @@
    ### 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):

    ```ts
    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.