colinhacks / zod

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

feat: add .upgrade #3677

Open MentalGear opened 3 months ago

MentalGear commented 3 months ago

.upgrade()

Necessity

Zod currently has no method to allow "append-only" additions of constraints to a z.object.

Use Case

One major case is "upgrading" (ie adding constraints on) pre-existing schemas that can't be manually edited.

Example: a Zod Schema is generated automatically each time a database schema changes. Obviously one can't change the generated file without having all edits overwritten on re-generation.

Alternatives considered

Using .shape && .extend is verbose and open for unintended changes.

.shape is illegibly verbose as it needs to be accesed via repeated object-dot-notion-access-chaining reference calls to the original object each time. This compounds linearly to how deep the object is.

.extend: overwrites the fields instead of appending on it. Extend could potentially add new objects or even overwrite existing ones complettely.

// example same with extend and with upgrade
const originalSchema = z.object({
  user: z.object({
    email: z.string(),
    age: z.number(),
    confirmed: z.boolean(),
    preferences: { theme: z.string() }
  }),
});

.shape & .extend version

const extendedSchema = originalSchema.extend({
  user: originalSchema.shape.user.extend({
    email: originalSchema.shape.user.shape.email.email(),
    age: originalSchema.shape.user.shape.age.min(18),
    confirmed: originalSchema.shape.user.shape.confirmed.default(false),
    preferences: originalSchema.shape.user.shape.preferences.theme.default('dark')
  }),
});

.upgrade version

const upgradedSchema = originalSchema.upgrade({
    user: (d) => d.upgrade({
        email: (d) => d.email(),
        age: (d) => d.min(18),
        confirmed: (d) => d.default(false),
        preferences: (d) => d.upgrade({
            theme: (d) => d.default("dark")
        })
    })
})

Advantages

Intuitive Interface

Type-Safety

Append-only

.upgrade prevents the unintended addition of fields. Example:

const schema = z.object({
    user: z.object({
        email: z.string().min(2)
    }),
})

const upgradedSchema = schema.upgrade({
    user: (user) => user.upgrade({
        email: (email) => email.email().min(5),
        // .upgrade will guard from this
        newField:(newField) => newField.string()
        // -----
    })
})

Protection

Field Protection

.upgrade prevents the unintended overwriting of fields.

const originalObj = z.object({
    name: z.string(),
    age: z.number(),
})

const upgradedObj = originalObj.upgrade({
    // these will error out
    name: (name) => 'Static Text',
    age: (age) => z.string(),
})

Constraint Protection

Trying to overwrite constraints will not work.

const originalObj = z.object({
    age: z.number().min(18),
})

const upgradedObj = originalObj.upgrade({
    // this won't register
    age: (age) => age.min(0),
})

Naming

Tests

An extensive test suite has been added. Please let me know whether you consider it a wide enough coverage.

Open issues

There seems to be still some issues with the types that I hope some Zod maintainer could help me with!

netlify[bot] commented 3 months ago

Deploy Preview for guileless-rolypoly-866f8a ready!

Built without sensitive environment variables

Name Link
Latest commit 60f0a9e93e0c436fdfa573b755529566f5105f55
Latest deploy log https://app.netlify.com/sites/guileless-rolypoly-866f8a/deploys/66a5126f84399b0008568006
Deploy Preview https://deploy-preview-3677--guileless-rolypoly-866f8a.netlify.app
Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify site configuration.

MentalGear commented 2 months ago

Hey @colinhacks: it would be great if you could take a look at the PR/RFC (and th x for your time and work on zod).