colinhacks / zod

TypeScript-first schema validation with static type inference
https://zod.dev
MIT License
33.61k stars 1.16k forks source link

ZodError#message cannot deal with circular data structures in its `issues` #3422

Open jandockx opened 6 months ago

jandockx commented 6 months ago

Example

A Mocha test that shows the issue:

/* eslint-env mocha */

const { z, ZodError } = require('zod')

describe('zod', function () {
  it('cannot deal with circular data structures', function () {
    const AnObjectSchema = z.object({ someLiteralProperty: z.literal(1) })

    const cicrularObject = {
      aProperty: 'a property',
      anotherProperty: 137,
      anObjectProperty: { anObjectPropertyProperty: 'an object property property' },
      anArrayProperty: [{ anArrayObjectPropertyProperty: 'an object property property' }]
    }
    cicrularObject.anObjectProperty.cicrularObject = cicrularObject
    cicrularObject.anArrayProperty.push(cicrularObject.anObjectProperty)
    const violatingObject = { someLiteralProperty: cicrularObject }

    const { success, error } = AnObjectSchema.safeParse(violatingObject)

    success.should.be.false()
    error.should.be.an.instanceof(ZodError)
    error.message.should.be.a.String()
  })
})

This test fails with

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Object'
    |     property 'anObjectProperty' -> object with constructor 'Object'
    --- property 'cicrularObject' closes the circle
    at JSON.stringify (<anonymous>)
    at get message [as message] (node_modules/zod/lib/ZodError.js:105:21)
    at Context.<anonymous> (test/00.zodIssue.js:23:11)
    at process.processImmediate (node:internal/timers:478:21)

Analysis

ZodError.ts, line 283:

  get message() {
    return JSON.stringify(this.issues, util.jsonStringifyReplacer, 2);
  }

JSON.stringify is called on this.issues. JSON.stringify cannot cope with circular data structures. In the example, the circular data structure appears in the received property of the issue.

jsonStringifyReplacer does not deal with this either:

helpers/util.ts#jsonStringifyReplacer, line 91:

  export const jsonStringifyReplacer = (_: string, value: any): any => {
    if (typeof value === "bigint") {
      return value.toString();
    }
    return value;
  };
colinhacks commented 6 months ago

Good catch. For security reasons, Zod isn't supposed to print the input data when it throws an error...but you've found the exception here with ZodInvalidLiteralIssue. This will be fixed in Zod 4.