-
Notifications
You must be signed in to change notification settings - Fork 412
Upgrade Zod peer dependency to v4 #327
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
88faa6a
3db719f
1b4f682
1d7fbe3
fe4ca2e
84bc0c6
203699c
35743d4
5a952cb
53d8ca4
962b334
f2dcb80
b15235d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -9,11 +9,11 @@ import { TypeChatJsonValidator } from '../typechat'; | |
| * the `getTypeName` method obtains the name of the given target type in the schema. | ||
| * @param schema A schema object where each property provides a name for an associated Zod type. | ||
| * @param targetType The name in the schema of the target type for JSON validation. | ||
| * @returns A `TypeChatJsonValidator<z.TypeOf<T[K]>>`, where T is the schema and K is the target type name. | ||
| * @returns A `TypeChatJsonValidator<z.infer<T[K]> & object>`, where T is the schema and K is the target type name. | ||
| */ | ||
| export function createZodJsonValidator<T extends Record<string, z.ZodType>, K extends keyof T & string>(schema: T, typeName: K): TypeChatJsonValidator<z.TypeOf<T[K]>> { | ||
| export function createZodJsonValidator<T extends Record<string, z.ZodType>, K extends keyof T & string>(schema: T, typeName: K): TypeChatJsonValidator<z.infer<T[K]> & object> { | ||
| let schemaText: string; | ||
| const validator: TypeChatJsonValidator<z.TypeOf<T[K]>> = { | ||
| const validator: TypeChatJsonValidator<z.infer<T[K]> & object> = { | ||
| getSchemaText: () => schemaText ??= getZodSchemaAsTypeScript(schema), | ||
| getTypeName: () => typeName, | ||
| validate | ||
|
|
@@ -25,22 +25,28 @@ export function createZodJsonValidator<T extends Record<string, z.ZodType>, K ex | |
| if (!result.success) { | ||
| return error(result.error.issues.map(({ path, message }) => `${path.map(key => `[${JSON.stringify(key)}]`).join("")}: ${message}`).join("\"")); | ||
| } | ||
| return success(result.data as z.TypeOf<T[K]>); | ||
| return success(result.data as z.infer<T[K]> & object); | ||
| } | ||
| } | ||
|
|
||
| function getTypeKind(type: z.ZodType) { | ||
| return (type._def as z.ZodTypeDef & { typeName: z.ZodFirstPartyTypeKind }).typeName; | ||
| // ZodTypeKind is the union of all type-discriminant strings defined by Zod v4. | ||
| // `z.core` is part of Zod v4's public API (exported from `zod/v4/classic/external.d.ts`). | ||
| // Using this type (rather than plain `string`) means the compiler enforces that | ||
| // every case label in switch statements is a legitimate Zod type kind. | ||
| type ZodTypeKind = z.core.$ZodTypeDef["type"]; | ||
|
|
||
| function getTypeKind(type: z.ZodType): ZodTypeKind { | ||
| return (type._zod.def as z.core.$ZodTypeDef).type; | ||
| } | ||
|
|
||
| function getTypeIdentity(type: z.ZodType): object { | ||
| switch (getTypeKind(type)) { | ||
| case z.ZodFirstPartyTypeKind.ZodObject: | ||
| return (type._def as z.ZodObjectDef).shape(); | ||
| case z.ZodFirstPartyTypeKind.ZodEnum: | ||
| return (type._def as z.ZodEnumDef).values; | ||
| case z.ZodFirstPartyTypeKind.ZodUnion: | ||
| return (type._def as z.ZodUnionDef).options; | ||
| case "object": | ||
| return (type._zod.def as z.core.$ZodObjectDef).shape; | ||
| case "enum": | ||
| return (type._zod.def as z.core.$ZodEnumDef).entries; | ||
| case "union": | ||
| return (type._zod.def as z.core.$ZodUnionDef).options; | ||
| } | ||
| return type; | ||
| } | ||
|
|
@@ -53,11 +59,10 @@ const enum TypePrecedence { | |
|
|
||
| function getTypePrecedence(type: z.ZodType): TypePrecedence { | ||
| switch (getTypeKind(type)) { | ||
| case z.ZodFirstPartyTypeKind.ZodEnum: | ||
| case z.ZodFirstPartyTypeKind.ZodUnion: | ||
| case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion: | ||
| case "enum": | ||
| case "union": // covers both z.union() and z.discriminatedUnion() — Zod v4 merged discriminated unions into the regular union type kind ("ZodDiscriminatedUnion" in v3) | ||
| return TypePrecedence.Union; | ||
| case z.ZodFirstPartyTypeKind.ZodIntersection: | ||
| case "intersection": | ||
| return TypePrecedence.Intersection; | ||
| } | ||
| return TypePrecedence.Object; | ||
|
|
@@ -82,16 +87,16 @@ export function getZodSchemaAsTypeScript(schema: Record<string, z.ZodType>): str | |
| if (result) { | ||
| appendNewLine(); | ||
| } | ||
| const description = type._def.description; | ||
| const description = type.description; | ||
| if (description) { | ||
| for (const comment of description.split("\n")) { | ||
| append(`// ${comment}`); | ||
| appendNewLine(); | ||
| } | ||
| } | ||
| if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodObject) { | ||
| if (getTypeKind(type) === "object") { | ||
| append(`interface ${name} `); | ||
| appendObjectType(type as z.ZodObject<z.ZodRawShape>); | ||
| appendObjectType(type as z.ZodObject); | ||
| } | ||
| else { | ||
| append(`type ${name} = `); | ||
|
|
@@ -130,66 +135,81 @@ export function getZodSchemaAsTypeScript(schema: Record<string, z.ZodType>): str | |
|
|
||
| function appendTypeDefinition(type: z.ZodType) { | ||
| switch (getTypeKind(type)) { | ||
| case z.ZodFirstPartyTypeKind.ZodString: | ||
| case "string": | ||
| return append("string"); | ||
| case z.ZodFirstPartyTypeKind.ZodNumber: | ||
| case "number": | ||
| case "int": | ||
| return append("number"); | ||
| case z.ZodFirstPartyTypeKind.ZodBoolean: | ||
| case "boolean": | ||
| return append("boolean"); | ||
| case z.ZodFirstPartyTypeKind.ZodDate: | ||
| case "date": | ||
| return append("Date"); | ||
| case z.ZodFirstPartyTypeKind.ZodUndefined: | ||
| case "undefined": | ||
| return append("undefined"); | ||
| case z.ZodFirstPartyTypeKind.ZodNull: | ||
| case "null": | ||
| return append("null"); | ||
| case z.ZodFirstPartyTypeKind.ZodUnknown: | ||
| case "unknown": | ||
| return append("unknown"); | ||
| case z.ZodFirstPartyTypeKind.ZodArray: | ||
| case "array": | ||
| return appendArrayType(type); | ||
| case z.ZodFirstPartyTypeKind.ZodObject: | ||
| case "object": | ||
| return appendObjectType(type); | ||
| case z.ZodFirstPartyTypeKind.ZodUnion: | ||
| return appendUnionOrIntersectionTypes((type._def as z.ZodUnionDef).options, TypePrecedence.Union); | ||
| case z.ZodFirstPartyTypeKind.ZodDiscriminatedUnion: | ||
| return appendUnionOrIntersectionTypes([...(type._def as z.ZodDiscriminatedUnionDef<string>).options.values()], TypePrecedence.Union); | ||
| case z.ZodFirstPartyTypeKind.ZodIntersection: | ||
| return appendUnionOrIntersectionTypes((type._def as z.ZodUnionDef).options, TypePrecedence.Intersection); | ||
| case z.ZodFirstPartyTypeKind.ZodTuple: | ||
| case "union": { // covers both z.union() and z.discriminatedUnion() — Zod v4 merged discriminated unions into the regular union type kind ("ZodDiscriminatedUnion" in v3); both have an `options` array | ||
| const unionDef = type._zod.def as z.core.$ZodDiscriminatedUnionDef | z.core.$ZodUnionDef; | ||
| return appendUnionOrIntersectionTypes(unionDef.options as readonly z.ZodType[], TypePrecedence.Union); | ||
| } | ||
| case "intersection": { | ||
| const intersectionDef = type._zod.def as z.core.$ZodIntersectionDef; | ||
| return appendUnionOrIntersectionTypes([intersectionDef.left as z.ZodType, intersectionDef.right as z.ZodType], TypePrecedence.Intersection); | ||
| } | ||
| case "tuple": | ||
|
Comment on lines
+164
to
+165
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in commit 962b334. Added
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in commit
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fixed in b15235d. The literal case now handles |
||
| return appendTupleType(type); | ||
| case z.ZodFirstPartyTypeKind.ZodRecord: | ||
| case "record": | ||
| return appendRecordType(type); | ||
| case z.ZodFirstPartyTypeKind.ZodLiteral: | ||
| return appendLiteral((type._def as z.ZodLiteralDef).value); | ||
| case z.ZodFirstPartyTypeKind.ZodEnum: | ||
| return append((type._def as z.ZodEnumDef).values.map(value => JSON.stringify(value)).join(" | ")); | ||
| case z.ZodFirstPartyTypeKind.ZodOptional: | ||
| return appendUnionOrIntersectionTypes([(type._def as z.ZodOptionalDef).innerType, z.undefined()], TypePrecedence.Union); | ||
| case z.ZodFirstPartyTypeKind.ZodReadonly: | ||
| case "literal": { | ||
| const litValues = (type._zod.def as z.core.$ZodLiteralDef<z.core.util.Literal>).values; | ||
| return append(litValues.map(v => v === null || typeof v === "string" || typeof v === "number" || typeof v === "boolean" ? JSON.stringify(v) : "any").join(" | ")); | ||
| } | ||
| case "enum": | ||
| return append(Object.values((type._zod.def as z.core.$ZodEnumDef).entries).map(value => JSON.stringify(value)).join(" | ")); | ||
| case "optional": | ||
| return appendUnionOrIntersectionTypes([(type._zod.def as z.core.$ZodOptionalDef).innerType as z.ZodType, z.undefined()], TypePrecedence.Union); | ||
| case "readonly": | ||
| return appendReadonlyType(type); | ||
| } | ||
| append("any"); | ||
| } | ||
|
|
||
| function appendArrayType(arrayType: z.ZodType) { | ||
| appendType((arrayType._def as z.ZodArrayDef).type, TypePrecedence.Object); | ||
| appendType((arrayType._zod.def as z.core.$ZodArrayDef).element as z.ZodType, TypePrecedence.Object); | ||
| append("[]"); | ||
| } | ||
|
|
||
| function appendObjectType(objectType: z.ZodType) { | ||
| append("{"); | ||
| appendNewLine(); | ||
| indent++; | ||
| for (let [name, type] of Object.entries((objectType._def as z.ZodObjectDef).shape())) { | ||
| const comment = type.description; | ||
| append(name); | ||
| if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodOptional) { | ||
| for (let [name, type] of Object.entries((objectType._zod.def as z.core.$ZodObjectDef).shape) as [string, z.ZodType][]) { | ||
| let inlineComment: string | undefined; | ||
| if (getTypeKind(type) === "optional") { | ||
| const wrapperDescription = type.description; | ||
| if (wrapperDescription) { | ||
| for (const line of wrapperDescription.split("\n")) { | ||
| append(`// ${line}`); | ||
| appendNewLine(); | ||
| } | ||
| } | ||
| append(name); | ||
| append("?"); | ||
| type = (type._def as z.ZodOptionalDef).innerType; | ||
| type = (type._zod.def as z.core.$ZodOptionalDef).innerType as z.ZodType; | ||
| } else { | ||
| inlineComment = type.description; | ||
| append(name); | ||
| } | ||
| append(": "); | ||
| appendType(type); | ||
| append(";"); | ||
| if (comment) append(` // ${comment}`); | ||
| if (inlineComment) append(` // ${inlineComment}`); | ||
| appendNewLine(); | ||
| } | ||
| indent--; | ||
|
|
@@ -208,42 +228,39 @@ export function getZodSchemaAsTypeScript(schema: Record<string, z.ZodType>): str | |
| function appendTupleType(tupleType: z.ZodType) { | ||
| append("["); | ||
| let first = true; | ||
| for (let type of (tupleType._def as z.ZodTupleDef<z.ZodTupleItems, z.ZodType>).items) { | ||
| const tupleDef = tupleType._zod.def as z.core.$ZodTupleDef; | ||
| for (let type of tupleDef.items as z.ZodType[]) { | ||
| if (!first) append(", "); | ||
| if (getTypeKind(type) === z.ZodFirstPartyTypeKind.ZodOptional) { | ||
| appendType((type._def as z.ZodOptionalDef).innerType, TypePrecedence.Object); | ||
| if (getTypeKind(type) === "optional") { | ||
| appendType((type._zod.def as z.core.$ZodOptionalDef).innerType as z.ZodType, TypePrecedence.Object); | ||
| append("?"); | ||
| } | ||
| else { | ||
| appendType(type); | ||
| } | ||
| first = false; | ||
| } | ||
| const rest = (tupleType._def as z.ZodTupleDef<z.ZodTupleItems, z.ZodType>).rest; | ||
| const rest = tupleDef.rest; | ||
| if (rest) { | ||
| if (!first) append(", "); | ||
| append("..."); | ||
| appendType(rest, TypePrecedence.Object); | ||
| appendType(rest as z.ZodType, TypePrecedence.Object); | ||
| append("[]"); | ||
| } | ||
| append("]"); | ||
| } | ||
|
|
||
| function appendRecordType(recordType: z.ZodType) { | ||
| append("Record<"); | ||
| appendType((recordType._def as z.ZodRecordDef).keyType); | ||
| appendType((recordType._zod.def as z.core.$ZodRecordDef).keyType as z.ZodType); | ||
| append(", "); | ||
| appendType((recordType._def as z.ZodRecordDef).valueType); | ||
| appendType((recordType._zod.def as z.core.$ZodRecordDef).valueType as z.ZodType); | ||
| append(">"); | ||
| } | ||
|
|
||
| function appendLiteral(value: unknown) { | ||
| append(typeof value === "string" || typeof value === "number" || typeof value === "boolean" ? JSON.stringify(value) : "any"); | ||
| } | ||
|
|
||
| function appendReadonlyType(readonlyType: z.ZodType) { | ||
| append("Readonly<"); | ||
| appendType((readonlyType._def as z.ZodReadonlyDef).innerType); | ||
| appendType((readonlyType._zod.def as z.core.$ZodReadonlyDef).innerType as z.ZodType); | ||
| append(">"); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.