? TFromPromise :
+ Type extends z.ZodRecord ? TFromRecord :
+ // Type extends z.ZodSet ? TFromSet :
+ Type extends z.ZodString ? TFromString :
+ Type extends z.ZodSymbol ? TFromSymbol :
+ Type extends z.ZodTemplateLiteral ? TFromTemplateLiteral :
+ Type extends z.ZodTransform ? TFromTransform : // suspicious
+ Type extends z.ZodTuple ? TFromTuple> :
+ Type extends z.ZodUndefined ? TFromUndefined :
+ Type extends z.ZodUnion ? TFromUnion :
+ Type extends z.ZodUnknown ? TFromUnknown :
+ Type extends z.ZodVoid ? TFromVoid :
+ // Intersection (Ensure Last Due to Zod Differentiation Issue)
+ Type extends z.ZodIntersection ? TFromIntersect :
+
+ t.TNever
+)
+
+// prettier-ignore
+function FromType(type: z.ZodType): t.TSchema {
+ if (type instanceof z.ZodString
+ || type instanceof z.ZodStringFormat
+ || IsStringInternals(type)) {
+ return FromStringLike(type);
+ }
+ if (type instanceof z.ZodAny) return FromAny(type);
+ if (type instanceof z.ZodArray) return FromArray(type);
+ if (type instanceof z.ZodBigInt) return FromBigInt(type);
+ if (type instanceof z.ZodBoolean) return FromBoolean(type);
+ if (type instanceof z.ZodCatch) return FromCatch(type);
+ // if (type instanceof z.ZodCustom) return FromCustom(type);
+ if (type instanceof z.ZodDate) return FromDate(type);
+ if (type instanceof z.ZodDefault) return FromDefault(type);
+ if (type instanceof z.ZodDiscriminatedUnion) return FromUnion(type);
+ if (type instanceof z.ZodEnum) return FromEnum(type);
+ if (type instanceof z.ZodIntersection) return FromIntersect(type);
+ if (type instanceof z.ZodLazy) return FromLazy(type);
+ if (type instanceof z.ZodLiteral) return FromLiteral(type);
+ // if (type instanceof z.ZodMap) return FromMap(type);
+ if (type instanceof z.ZodNullable) return FromNullable(type);
+ if (type instanceof z.ZodNumber) return FromNumber(type);
+ if (type instanceof z.ZodObject) return FromObject(type);
+ if (type instanceof z.ZodOptional) return FromOptional(type);
+ // if (type instanceof z.ZodPipe) return FromPipe(type);
+ if (type instanceof z.ZodPromise) return FromPromise(type);
+ if (type instanceof z.ZodReadonly) return FromReadonly(type);
+ if (type instanceof z.ZodRecord) return FromRecord(type);
+ // if (type instanceof z.ZodSet) return FromSet(type);
+ if (type instanceof z.ZodSymbol) return FromSymbol(type);
+ if (type instanceof z.ZodTemplateLiteral) return FromTemplateLiteral(type);
+ if (type instanceof z.ZodTransform) return FromTransform(type);
+ if (type instanceof z.ZodTuple) return FromTuple(type);
+ if (type instanceof z.ZodUnion) return FromUnion(type);
+ if (type instanceof z.ZodUnknown) return FromUnknown(type);
+ if (type instanceof z.ZodUndefined) return FromUndefined(type);
+ if (type instanceof z.ZodVoid) return FromVoid(type);
+ return t.Never();
+}
+
+// ------------------------------------------------------------------
+// TypeBoxFromZod
+// ------------------------------------------------------------------
+/** Creates a TypeBox type from Zod v4 */
+// prettier-ignore
+export type TTypeBoxFromZod4
+> = Result
+/** Creates a TypeBox type from Zod v4 */
+export function TypeBoxFromZod4(type: Type): TTypeBoxFromZod4 {
+ return FromType(type) as never
+}
diff --git a/src/typebox/typebox.ts b/src/typebox/typebox.ts
index 9cbaaf4..acbbbf4 100644
--- a/src/typebox/typebox.ts
+++ b/src/typebox/typebox.ts
@@ -30,6 +30,7 @@ import { type TTypeBoxFromSyntax, TypeBoxFromSyntax } from './typebox-from-synta
import { type TTypeBoxFromTypeBox, TypeBoxFromTypeBox } from './typebox-from-typebox'
import { type TTypeBoxFromValibot, TypeBoxFromValibot } from './typebox-from-valibot'
import { type TTypeBoxFromZod, TypeBoxFromZod } from './typebox-from-zod'
+import { type TTypeBoxFromZod4, TypeBoxFromZod4 } from './typebox-from-zod4'
import { type TSyntaxOptions } from '../options'
import * as g from '../guard'
@@ -71,6 +72,7 @@ export type TTypeBox, Type> :
Type extends g.TypeBoxType ? TTypeBoxFromTypeBox :
Type extends g.ValibotType ? TTypeBoxFromValibot :
+ Type extends g.Zod4Type ? TTypeBoxFromZod4 :
Type extends g.ZodType ? TTypeBoxFromZod :
t.TNever
)> = Result
@@ -86,6 +88,7 @@ export function TypeBox(...args: any[]): never {
g.IsSyntax(type) ? TypeBoxFromSyntax(ContextFromParameter(parameter), type, options) :
g.IsTypeBox(type) ? TypeBoxFromTypeBox(type) :
g.IsValibot(type) ? TypeBoxFromValibot(type) :
+ g.IsZod4(type) ? TypeBoxFromZod4(type) :
g.IsZod(type) ? TypeBoxFromZod(type) :
t.Never()
) as never
diff --git a/src/valibot/valibot-from-zod4.ts b/src/valibot/valibot-from-zod4.ts
new file mode 100644
index 0000000..cb5be62
--- /dev/null
+++ b/src/valibot/valibot-from-zod4.ts
@@ -0,0 +1,53 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import { type TTypeBoxFromZod4, TypeBoxFromZod4 } from '../typebox/typebox-from-zod4'
+import { type TValibotFromTypeBox, ValibotFromTypeBox } from './valibot-from-typebox'
+
+import * as t from '@sinclair/typebox'
+import * as v from 'valibot'
+import { z } from 'zod/v4'
+
+// ------------------------------------------------------------------
+// ValibotFromZod4
+// ------------------------------------------------------------------
+/** Creates a Valibot type from Zod v4 */
+// prettier-ignore
+export type TValibotFromZod4,
+ Result extends v.BaseSchema = TValibotFromTypeBox
+> = Result
+/** Creates a Valibot type from Zod v4 */
+// prettier-ignore
+export function ValibotFromZod4 = TValibotFromZod4
+>(type: Type): Result {
+ const schema = TypeBoxFromZod4(type)
+ const result = ValibotFromTypeBox(schema)
+ return result as never
+}
diff --git a/src/zod/zod-from-zod4.ts b/src/zod/zod-from-zod4.ts
new file mode 100644
index 0000000..23cbc1b
--- /dev/null
+++ b/src/zod/zod-from-zod4.ts
@@ -0,0 +1,53 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import { type TTypeBoxFromZod4, TypeBoxFromZod4 } from '../typebox/typebox-from-zod4'
+import { type TZodFromTypeBox, ZodFromTypeBox } from './zod-from-typebox'
+
+import * as t from '@sinclair/typebox'
+import * as z from 'zod'
+import { z as z4 } from 'zod/v4'
+
+// ------------------------------------------------------------------
+// ZodFromZod4
+// ------------------------------------------------------------------
+/** Creates a Zod v3 type from Zod v4 */
+// prettier-ignore
+export type TZodFromZod4,
+ Result extends z.ZodTypeAny | z.ZodEffects = TZodFromTypeBox
+> = Result
+/** Creates a Zod v3 type from Zod v4 */
+// prettier-ignore
+export function ZodFromZod4 = TZodFromZod4
+>(type: Type): Result {
+ const schema = TypeBoxFromZod4(type)
+ const result = ZodFromTypeBox(schema)
+ return result as never
+}
diff --git a/src/zod/zod.ts b/src/zod/zod.ts
index 7bb96b3..f647cda 100644
--- a/src/zod/zod.ts
+++ b/src/zod/zod.ts
@@ -30,6 +30,7 @@ import { type TZodFromSyntax, ZodFromSyntax } from './zod-from-syntax'
import { type TZodFromTypeBox, ZodFromTypeBox } from './zod-from-typebox'
import { type TZodFromValibot, ZodFromValibot } from './zod-from-valibot'
import { type TZodFromZod, ZodFromZod } from './zod-from-zod'
+import { type TZodFromZod4, ZodFromZod4 } from './zod-from-zod4'
import { type TSyntaxOptions } from '../options'
import { type TParameter, type TContextFromParameter, ContextFromParameter } from '../typebox/typebox'
@@ -47,6 +48,7 @@ export type TZod :
Type extends g.ValibotType ? TZodFromValibot :
Type extends g.ZodType ? TZodFromZod :
+ Type extends g.Zod4Type ? TZodFromZod4 :
z.ZodNever
)> = Result
@@ -65,6 +67,7 @@ export function Zod(...args: any[]): never {
g.IsTypeBox(type) ? ZodFromTypeBox(type) :
g.IsValibot(type) ? ZodFromValibot(type) :
g.IsZod(type) ? ZodFromZod(type) :
+ g.IsZod4(type) ? ZodFromZod4(type) :
z.never()
) as never
}
diff --git a/src/zod4/zod4-from-syntax.ts b/src/zod4/zod4-from-syntax.ts
new file mode 100644
index 0000000..d36be1f
--- /dev/null
+++ b/src/zod4/zod4-from-syntax.ts
@@ -0,0 +1,48 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import { TTypeBoxFromSyntax, TypeBoxFromSyntax } from '../typebox/typebox-from-syntax'
+import { Zod4FromTypeBox, TZod4FromTypeBox } from './zod4-from-typebox'
+import * as t from '@sinclair/typebox'
+import { z } from 'zod/v4'
+
+// ------------------------------------------------------------------
+// Zod4FromSyntax
+// ------------------------------------------------------------------
+/** Creates a Zod v4 type from Syntax */
+// prettier-ignore
+export type TZod4FromSyntax,
+ Result extends z.ZodTypeAny | z.ZodNever = TZod4FromTypeBox
+> = Result
+/** Creates a Zod v4 type from Syntax */
+export function Zod4FromSyntax(context: Context, type: Type, options?: t.SchemaOptions): TZod4FromSyntax {
+ const typebox = TypeBoxFromSyntax(context, type, options)
+ const result = Zod4FromTypeBox(typebox)
+ return result as never
+}
diff --git a/src/zod4/zod4-from-typebox.ts b/src/zod4/zod4-from-typebox.ts
new file mode 100644
index 0000000..e42b0f6
--- /dev/null
+++ b/src/zod4/zod4-from-typebox.ts
@@ -0,0 +1,433 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import * as t from '@sinclair/typebox'
+import z4 from 'zod/dist/types/v4'
+import { z } from 'zod/v4'
+
+// ------------------------------------------------------------------
+// Constraint
+// ------------------------------------------------------------------
+type TConstraint = (input: Input) => Output
+
+// ------------------------------------------------------------------
+// Any
+// ------------------------------------------------------------------
+type TFromAny = Result
+function FromAny(_type: t.TAny): z.ZodType {
+ return z.any()
+}
+// ------------------------------------------------------------------
+// Array
+// ------------------------------------------------------------------
+type TFromArray>> = Result
+function FromArray(type: t.TArray): z.ZodType {
+ const constraints: TConstraint>[] = []
+ const { minItems, maxItems /* minContains, maxContains, contains */ } = type
+ if (t.ValueGuard.IsNumber(minItems)) constraints.push((input) => input.min(minItems))
+ if (t.ValueGuard.IsNumber(maxItems)) constraints.push((input) => input.max(maxItems))
+ const mapped = z.array(FromType(type.items))
+ return constraints.reduce((type, constraint) => constraint(type), mapped)
+}
+// ------------------------------------------------------------------
+// BigInt
+// ------------------------------------------------------------------
+type TFromBigInt = Result
+function FromBigInt(_type: t.TBigInt): z.ZodType {
+ return z.bigint()
+}
+// ------------------------------------------------------------------
+// Boolean
+// ------------------------------------------------------------------
+type TFromBoolean = Result
+function FromBoolean(_type: t.TBoolean): z.ZodType {
+ return z.boolean()
+}
+// ------------------------------------------------------------------
+// Date
+// ------------------------------------------------------------------
+type TFromDate = Result
+function FromDate(_type: t.TDate): z.ZodType {
+ return z.date()
+}
+// ------------------------------------------------------------------
+// Function
+// ------------------------------------------------------------------
+// note: Zod v4 does not support function types, so we use a workaround
+// z.custom[0]>((fn) => schema.implement(fn))
+// https://github.com/colinhacks/zod/issues/4143#issuecomment-2845134912
+// prettier-ignore
+type TFromFunction
+> = (
+ MappedParameters extends [z.ZodType, ...z.ZodType[]] | []
+ ? z.ZodType<(...args: z.output>) => z.output>>
+ : z.ZodNever
+)
+function FromFunction(type: t.TFunction): z.ZodType {
+ const mappedParameters = FromTypes(type.parameters)
+ const input = z.tuple(mappedParameters as [z.ZodType, ...z.ZodType[]])
+ const output = FromType(type.returns)
+
+ return z.custom().transform((arg, ctx) => {
+ if (typeof arg !== 'function') {
+ ctx.addIssue({
+ code: "invalid_type",
+ expected: 'custom',
+ received: typeof arg,
+ input: arg
+ })
+ return z.NEVER
+ }
+
+ // Use function schema internally for validation
+ const functionSchema = z.function({
+ input,
+ output
+ })
+
+ return functionSchema.implement(arg as (...args: any[]) => any)
+ })
+}
+// ------------------------------------------------------------------
+// Integer
+// ------------------------------------------------------------------
+type TFromInteger = Result
+function FromInteger(type: t.TInteger): z.ZodType {
+ const { exclusiveMaximum, exclusiveMinimum, minimum, maximum, multipleOf } = type
+ const constraints: TConstraint[] = [(value) => value.int()]
+ if (t.ValueGuard.IsNumber(exclusiveMinimum)) constraints.push((input) => input.min(exclusiveMinimum + 1))
+ if (t.ValueGuard.IsNumber(exclusiveMaximum)) constraints.push((input) => input.min(exclusiveMaximum - 1))
+ if (t.ValueGuard.IsNumber(maximum)) constraints.push((input) => input.max(maximum))
+ if (t.ValueGuard.IsNumber(minimum)) constraints.push((input) => input.min(minimum))
+ if (t.ValueGuard.IsNumber(multipleOf)) constraints.push((input) => input.multipleOf(multipleOf))
+ return constraints.reduce((input, constraint) => constraint(input), z.number())
+}
+// ------------------------------------------------------------------
+// Intersect
+// ------------------------------------------------------------------
+// prettier-ignore
+type TFromIntersect = (
+ Types extends [infer Left extends t.TSchema, ...infer Right extends t.TSchema[]]
+ ? TFromIntersect, Result>>
+ : Result
+)
+function FromIntersect(type: t.TIntersect): z.ZodType {
+ return type.allOf.reduce((result, left) => {
+ return z.intersection(FromType(left), result) as never
+ }, z.unknown()) as never
+}
+// ------------------------------------------------------------------
+// Literal
+// ------------------------------------------------------------------
+type TFromLiteral> = Result
+function FromLiteral(type: t.TLiteral): z.ZodType {
+ return z.literal(type.const)
+}
+// ------------------------------------------------------------------
+// Object
+// ------------------------------------------------------------------
+// prettier-ignore
+type TFromObject< Properties extends t.TProperties,
+ Result = z.ZodObject<{
+ [Key in keyof Properties]: TFromType
+ }>,
+> = Result
+// prettier-ignore
+function FromObject(type: t.TObject): z.ZodType {
+ const constraints: TConstraint>[] = []
+ const { additionalProperties } = type
+ if (additionalProperties === false) constraints.push((input) => input.strict())
+ if (t.KindGuard.IsSchema(additionalProperties)) constraints.push((input) => input.catchall(FromType(additionalProperties)))
+ const properties = globalThis.Object.getOwnPropertyNames(type.properties).reduce((result, key) => ({ ...result, [key]: FromType(type.properties[key]) }), {})
+ return constraints.reduce((type, constraint) => constraint(type), z.object(properties))
+}
+// ------------------------------------------------------------------
+// Promise
+// ------------------------------------------------------------------
+type TFromPromise>> = Result
+function FromPromise(type: t.TPromise): z.ZodType {
+ return z.promise(FromType(type.item))
+}
+// ------------------------------------------------------------------
+// Record
+// ------------------------------------------------------------------
+type TFromRegExp = Result
+function FromRegExp(type: t.TRegExp): z.ZodType {
+ const constraints: TConstraint[] = [(input) => input.regex(new RegExp(type.source), type.flags)]
+ const { minLength, maxLength } = type
+ if (t.ValueGuard.IsNumber(maxLength)) constraints.push((input) => input.max(maxLength))
+ if (t.ValueGuard.IsNumber(minLength)) constraints.push((input) => input.min(minLength))
+ return constraints.reduce((type, constraint) => constraint(type), z.string())
+}
+// ------------------------------------------------------------------
+// Record
+// ------------------------------------------------------------------
+// prettier-ignore
+type TFromRecord = (
+ TFromType extends infer ZodKey extends z4.core.$ZodRecordKey
+ ? z.ZodRecord>
+ : z.ZodNever
+)
+// prettier-ignore
+function FromRecord(type: t.TRecord): z.ZodType {
+ const pattern = globalThis.Object.getOwnPropertyNames(type.patternProperties)[0]
+ const value = FromType(type.patternProperties[pattern])
+ return (
+ pattern === t.PatternNumberExact ? z.record(z.number(), value) :
+ pattern === t.PatternStringExact ? z.record(z.string(), value) :
+ z.record(z.string().regex(new RegExp(pattern)), value)
+ )
+}
+// ------------------------------------------------------------------
+// Never
+// ------------------------------------------------------------------
+type TFromNever = Result
+function FromNever(type: t.TNever): z.ZodType {
+ return z.never()
+}
+// ------------------------------------------------------------------
+// Never
+// ------------------------------------------------------------------
+type TFromNull = Result
+function FromNull(_type: t.TNull): z.ZodType {
+ return z.null()
+}
+// ------------------------------------------------------------------
+// Number
+// ------------------------------------------------------------------
+type TFromNumber = Result
+function FromNumber(type: t.TNumber): z.ZodType {
+ const { exclusiveMaximum, exclusiveMinimum, minimum, maximum, multipleOf } = type
+ const constraints: TConstraint[] = []
+ if (t.ValueGuard.IsNumber(exclusiveMinimum)) constraints.push((input) => input.min(exclusiveMinimum + 1))
+ if (t.ValueGuard.IsNumber(exclusiveMaximum)) constraints.push((input) => input.min(exclusiveMaximum - 1))
+ if (t.ValueGuard.IsNumber(maximum)) constraints.push((input) => input.max(maximum))
+ if (t.ValueGuard.IsNumber(minimum)) constraints.push((input) => input.min(minimum))
+ if (t.ValueGuard.IsNumber(multipleOf)) constraints.push((input) => input.multipleOf(multipleOf))
+ return constraints.reduce((input, constraint) => constraint(input), z.number())
+}
+// ------------------------------------------------------------------
+// String
+// ------------------------------------------------------------------
+type TFromString = Result
+// prettier-ignore
+function FromString(type: t.TString): z.ZodType {
+ const constraints: TConstraint[] = []
+ const { minLength, maxLength, pattern, format } = type
+ const input = t.ValueGuard.IsString(format) ?
+ format === 'base64' ? z.base64() :
+ format === 'base64url' ? z.base64url() :
+ format === 'cidrv4' ? z.cidrv4() :
+ format === 'cidrv6' ? z.cidrv6() :
+ format === 'cidr' ? z.union([z.cidrv4(), z.cidrv6()]) :
+ format === 'cuid' ? z.cuid() :
+ format === 'cuid2' ? z.cuid2() :
+ format === 'date' ? z.date() :
+ format === 'datetime' ? z.iso.datetime() :
+ format === 'duration' ? z.iso.duration() :
+ format === 'email' ? z.email() :
+ format === 'emoji' ? z.emoji() :
+ format === 'ipv4' ? z.ipv4() :
+ format === 'ipv6' ? z.ipv6() :
+ format === 'ip' ? z.union([z.ipv4(), z.ipv6()]) :
+ format === 'jwt' ? z.jwt() :
+ format === 'nanoid' ? z.nanoid() :
+ format === 'time' ? z.iso.time() :
+ format === 'ulid' ? z.ulid() :
+ format === 'url' ? z.url() :
+ format === 'uuid' ? z.uuid() :
+ z.string() : z.string();
+
+ if (t.ValueGuard.IsNumber(maxLength)) constraints.push((input) => input.max(maxLength))
+ if (t.ValueGuard.IsNumber(minLength)) constraints.push((input) => input.min(minLength))
+ if (t.ValueGuard.IsString(pattern)) constraints.push((input) => input.regex(new RegExp(pattern)))
+ return input instanceof z.ZodString
+ ? constraints.reduce((type, constraint) => constraint(type), input)
+ : input;
+}
+// ------------------------------------------------------------------
+// Symbol
+// ------------------------------------------------------------------
+type TFromSymbol = Result
+function FromSymbol(_type: t.TSymbol): z.ZodType {
+ return z.symbol()
+}
+// ------------------------------------------------------------------
+// Tuple
+// ------------------------------------------------------------------
+// prettier-ignore
+type TFromTuple> = (
+ Mapped extends [z.ZodType, ...z.ZodType[]] | []
+ ? z.ZodTuple
+ : z.ZodNever
+)
+function FromTuple(type: t.TTuple): z.ZodType {
+ const mapped = FromTypes(type.items || []);
+ return z.tuple(mapped as [z.ZodType, ...z.ZodType[]])
+}
+// ------------------------------------------------------------------
+// Undefined
+// ------------------------------------------------------------------
+type TFromUndefined = Result
+function FromUndefined(_type: t.TUndefined): z.ZodType {
+ return z.undefined()
+}
+// ------------------------------------------------------------------
+// Union
+// ------------------------------------------------------------------
+// prettier-ignore
+type TFromUnion> = (
+ Mapped extends z.ZodUnion ? z.ZodUnion : z.ZodNever
+)
+function FromUnion(_type: t.TUnion): z.ZodType {
+ const mapped = FromTypes(_type.anyOf) as [z.ZodType, z.ZodType, ...z.ZodType[]]
+ return mapped.length >= 1 ? z.union(mapped) : z.never()
+}
+// ------------------------------------------------------------------
+// TUnknown
+// ------------------------------------------------------------------
+type TFromUnknown = Result
+function FromUnknown(_type: t.TUnknown): z.ZodType {
+ return z.unknown()
+}
+// ------------------------------------------------------------------
+// Void
+// ------------------------------------------------------------------
+type TFromVoid = Result
+function FromVoid(_type: t.TVoid): z.ZodType {
+ return z.void()
+}
+// ------------------------------------------------------------------
+// Types
+// ------------------------------------------------------------------
+// prettier-ignore
+type TFromTypes = (
+ Types extends [infer Left extends t.TSchema, ...infer Right extends t.TSchema[]]
+ ? TFromTypes]>
+ : Result
+)
+function FromTypes(types: t.TSchema[]): Readonly {
+ return types.map((type) => FromType(type)) as Readonly
+}
+// ------------------------------------------------------------------
+// Type
+// ------------------------------------------------------------------
+// prettier-ignore
+type TFromType ? TFromArray :
+ Type extends t.TBigInt ? TFromBigInt :
+ Type extends t.TBoolean ? TFromBoolean :
+ Type extends t.TDate ? TFromDate :
+ Type extends t.TFunction ? TFromFunction :
+ Type extends t.TInteger ? TFromInteger :
+ Type extends t.TIntersect ? TFromIntersect :
+ Type extends t.TLiteral ? TFromLiteral :
+ Type extends t.TNever ? TFromNever :
+ Type extends t.TNull ? TFromNull :
+ Type extends t.TNumber ? TFromNumber :
+ Type extends t.TObject ? TFromObject :
+ Type extends t.TPromise ? TFromPromise :
+ Type extends t.TRecord ? TFromRecord :
+ Type extends t.TRegExp ? TFromRegExp :
+ Type extends t.TString ? TFromString :
+ Type extends t.TSymbol ? TFromSymbol :
+ Type extends t.TTuple ? TFromTuple :
+ Type extends t.TUndefined ? TFromUndefined :
+ Type extends t.TUnion ? TFromUnion :
+ Type extends t.TUnknown ? TFromUnknown :
+ Type extends t.TVoid ? TFromVoid :
+ z.ZodNever
+ ),
+ // Modifier Mapping
+ IsReadonly extends boolean = Type extends t.TReadonly ? true : false,
+ IsOptional extends boolean = Type extends t.TOptional ? true : false,
+ Result extends z.ZodType | z.ZodNever = (
+ [IsReadonly, IsOptional] extends [true, true] ? z.ZodReadonly> :
+ [IsReadonly, IsOptional] extends [false, true] ? z.ZodOptional :
+ [IsReadonly, IsOptional] extends [true, false] ? z.ZodReadonly :
+ Mapped
+ )
+> = Result
+// prettier-ignore
+function FromType(type: t.TSchema): z.ZodType | z.ZodNever {
+ const constraints: TConstraint[] = []
+ if(!t.ValueGuard.IsUndefined(type.description)) constraints.push(input => input.describe(type.description!))
+ if(!t.ValueGuard.IsUndefined(type.default)) constraints.push(input => input.default(type.default))
+ const mapped = constraints.reduce((type, constraint) => constraint(type), (
+ t.KindGuard.IsAny(type) ? FromAny(type) :
+ t.KindGuard.IsArray(type) ? FromArray(type) :
+ t.KindGuard.IsBigInt(type) ? FromBigInt(type) :
+ t.KindGuard.IsBoolean(type) ? FromBoolean(type) :
+ t.KindGuard.IsDate(type) ? FromDate(type) :
+ t.KindGuard.IsFunction(type) ? FromFunction(type) :
+ t.KindGuard.IsInteger(type) ? FromInteger(type) :
+ t.KindGuard.IsIntersect(type) ? FromIntersect(type) :
+ t.KindGuard.IsLiteral(type) ? FromLiteral(type) :
+ t.KindGuard.IsNever(type) ? FromNever(type) :
+ t.KindGuard.IsNull(type) ? FromNull(type) :
+ t.KindGuard.IsNumber(type) ? FromNumber(type) :
+ t.KindGuard.IsObject(type) ? FromObject(type) :
+ t.KindGuard.IsPromise(type) ? FromPromise(type) :
+ t.KindGuard.IsRegExp(type) ? FromRegExp(type) :
+ t.KindGuard.IsRecord(type) ? FromRecord(type) :
+ t.KindGuard.IsString(type) ? FromString(type) :
+ t.KindGuard.IsSymbol(type) ? FromSymbol(type) :
+ t.KindGuard.IsTuple(type) ? FromTuple(type) :
+ t.KindGuard.IsUndefined(type) ? FromUndefined(type) :
+ t.KindGuard.IsUnion(type) ? FromUnion(type) :
+ t.KindGuard.IsUnknown(type) ? FromUnknown(type) :
+ t.KindGuard.IsVoid(type) ? FromVoid(type) :
+ z.never()
+ ))
+ // Modifier Mapping
+ const isOptional = t.KindGuard.IsOptional(type)
+ const isReadonly = t.KindGuard.IsReadonly(type)
+ const result = (
+ isOptional && isReadonly ? z.optional(mapped) :
+ isOptional && !isReadonly ? z.optional(mapped) :
+ !isOptional && isReadonly ? mapped :
+ mapped
+ )
+ return result
+}
+// ------------------------------------------------------------------
+// ZodFromTypeBox
+// ------------------------------------------------------------------
+/** Creates a Zod type from TypeBox */
+// prettier-ignore
+export type TZod4FromTypeBox
+> = Result
+/** Creates a Zod v4 type from TypeBox */
+export function Zod4FromTypeBox(type: Type): TZod4FromTypeBox {
+ return FromType(type) as never
+}
diff --git a/src/zod4/zod4-from-valibot.ts b/src/zod4/zod4-from-valibot.ts
new file mode 100644
index 0000000..ba324df
--- /dev/null
+++ b/src/zod4/zod4-from-valibot.ts
@@ -0,0 +1,49 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import { type TTypeBoxFromValibot, TypeBoxFromValibot } from '../typebox/typebox-from-valibot'
+import { type TZod4FromTypeBox, Zod4FromTypeBox } from './zod4-from-typebox'
+
+import * as t from '@sinclair/typebox'
+import * as v from 'valibot'
+import { z } from 'zod/v4'
+
+/** Creates a Zod v4 type from Valibot */
+// prettier-ignore
+export type TZod4FromValibot,
+ TypeBox extends t.TSchema = TTypeBoxFromValibot,
+ Result extends z.ZodTypeAny | z.ZodNever = TZod4FromTypeBox
+> = Result
+
+/** Creates a Zod v4 type from Valibot */
+// prettier-ignore
+export function Zod4FromValibot>(type: Type): TZod4FromValibot {
+ const typebox = TypeBoxFromValibot(type)
+ const result = Zod4FromTypeBox(typebox)
+ return result
+}
diff --git a/src/zod4/zod4-from-zod.ts b/src/zod4/zod4-from-zod.ts
new file mode 100644
index 0000000..847ba2a
--- /dev/null
+++ b/src/zod4/zod4-from-zod.ts
@@ -0,0 +1,53 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import { type TTypeBoxFromZod, TypeBoxFromZod } from '../typebox/typebox-from-zod'
+import { type TZod4FromTypeBox, Zod4FromTypeBox } from './zod4-from-typebox'
+
+import * as t from '@sinclair/typebox'
+import * as z from 'zod'
+import { z as z4 } from 'zod/v4'
+
+// ------------------------------------------------------------------
+// Zod4FromZod
+// ------------------------------------------------------------------
+/** Creates a Zod v4 type from Zod v3 */
+// prettier-ignore
+export type TZod4FromZod,
+ TypeBox extends t.TSchema = TTypeBoxFromZod,
+ Result extends z4.ZodType | z.ZodNever = TZod4FromTypeBox
+> = Result
+/** Creates a Zod v4 type from Zod v3 */
+// prettier-ignore
+export function Zod4FromZod,
+ Result extends z4.ZodType | z.ZodNever = TZod4FromZod
+>(type: Type): Result {
+ const schema = TypeBoxFromZod(type)
+ const result = Zod4FromTypeBox(schema)
+ return result as never
+}
diff --git a/src/zod4/zod4-from-zod4.ts b/src/zod4/zod4-from-zod4.ts
new file mode 100644
index 0000000..b3bf6f6
--- /dev/null
+++ b/src/zod4/zod4-from-zod4.ts
@@ -0,0 +1,41 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import { z } from 'zod/v4'
+
+/** Creates a Zod v4 type from Zod v4 */
+// prettier-ignore
+export type TZod4FromZod4 = Result
+
+/** Creates a Zod v4 type from Zod v4 */
+// prettier-ignore
+export function Zod4FromZod4(type: Type): TZod4FromZod4 {
+ return type as never
+}
diff --git a/src/zod4/zod4.ts b/src/zod4/zod4.ts
new file mode 100644
index 0000000..ab9eef9
--- /dev/null
+++ b/src/zod4/zod4.ts
@@ -0,0 +1,73 @@
+/*--------------------------------------------------------------------------
+
+@sinclair/typemap
+
+The MIT License (MIT)
+
+Copyright (c) 2024-2025 Haydn Paterson (sinclair)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+---------------------------------------------------------------------------*/
+
+import { type TZod4FromSyntax, Zod4FromSyntax } from './zod4-from-syntax'
+import { type TZod4FromTypeBox, Zod4FromTypeBox } from './zod4-from-typebox'
+import { type TZod4FromValibot, Zod4FromValibot } from './zod4-from-valibot'
+import { type TZod4FromZod4, Zod4FromZod4 } from './zod4-from-zod4'
+import { type TZod4FromZod, Zod4FromZod } from './zod4-from-zod'
+import { type TSyntaxOptions } from '../options'
+
+import { type TParameter, type TContextFromParameter, ContextFromParameter } from '../typebox/typebox'
+
+import * as g from '../guard'
+import { z } from 'zod/v4'
+
+// ------------------------------------------------------------------
+// Zod4
+// ------------------------------------------------------------------
+/** Creates a Zod v4 type by mapping from a remote Type */
+// prettier-ignore
+export type TZod4, Type> :
+ Type extends g.TypeBoxType ? TZod4FromTypeBox :
+ Type extends g.ValibotType ? TZod4FromValibot :
+ Type extends g.ZodType ? TZod4FromZod :
+ Type extends g.Zod4Type ? TZod4FromZod4 :
+ z.ZodNever
+)> = Result
+
+/** Creates a Zod v4 type by mapping from a remote Type */
+export function Zod4(parameter: Parameter, type: Type, options?: TSyntaxOptions): TZod4
+/** Creates a Zod v4 type by mapping from a remote Type */
+export function Zod4(type: Type, options?: TSyntaxOptions): TZod4<{}, Type>
+/** Creates a Zod v4 type by mapping from a remote Type */
+export function Zod4(type: Type, options?: TSyntaxOptions): TZod4<{}, Type>
+/** Creates a Zod v4 type by mapping from a remote Type */
+// prettier-ignore
+export function Zod4(...args: any[]): never {
+ const [parameter, type, options] = g.Signature(args)
+ return (
+ g.IsSyntax(type) ? Zod4FromSyntax(ContextFromParameter(parameter), type, options) :
+ g.IsTypeBox(type) ? Zod4FromTypeBox(type) :
+ g.IsValibot(type) ? Zod4FromValibot(type) :
+ g.IsZod(type) ? Zod4FromZod(type) :
+ g.IsZod4(type) ? Zod4FromZod4(type) :
+ z.never()
+ ) as never
+}
diff --git a/task/benchmark/index.ts b/task/benchmark/index.ts
index f071ea0..bffaaec 100644
--- a/task/benchmark/index.ts
+++ b/task/benchmark/index.ts
@@ -1,8 +1,9 @@
import { Value } from '@sinclair/typebox/value'
-import { Compile, TypeBox } from '@sinclair/typemap'
+import { Compile, TypeBox, TypeBoxFromZod4, Zod4 } from '@sinclair/typemap'
import * as v from 'valibot'
import * as z from 'zod'
+import * as z4 from 'zod/v4'
// ------------------------------------------------------------------
// Benchmark
@@ -76,7 +77,50 @@ function valibot_using_compiler() {
return benchmark('valibot', 'typebox:compile', () => T.Check({ x: 'hello', y: 42, z: true }))
}
+// ------------------------------------------------------------------
+// Zod v4
+// ------------------------------------------------------------------
+function zod4() {
+ const T = z4.object({
+ x: z4.string(),
+ y: z4.number(),
+ z: z4.boolean(),
+ })
+ return benchmark('zod v4', 'zod v4', () => T.safeParse({ x: 'hello', y: 42, z: true }).success)
+}
+function zod4_using_value() {
+ const T = TypeBox(
+ z4.object({
+ x: z4.string(),
+ y: z4.number(),
+ z: z4.boolean(),
+ }),
+ )
+ return benchmark('zod v4', 'typebox:value', () => Value.Check(T, { x: 'hello', y: 42, z: true }))
+}
+function zod4_using_compiler() {
+ const T = Compile(
+ z4.object({
+ x: z4.string(),
+ y: z4.number(),
+ z: z4.boolean(),
+ }),
+ )
+ return benchmark('zod v4', 'typebox:compile', () => T.Check({ x: 'hello', y: 42, z: true }))
+}
+
+// Direct API comparison
+function zod4_using_library_api() {
+ const T = Zod4(`{
+ x: string,
+ y: number,
+ z: boolean
+ }`)
+ return benchmark('zod v4', 'typemap:zod4', () => T.safeParse({ x: 'hello', y: 42, z: true }).success)
+}
+
console.log('running benchmark')
console.table([valibot(), valibot_using_value(), valibot_using_compiler()])
console.table([zod(), zod_using_value(), zod_using_compiler()])
+console.table([zod4(), zod4_using_library_api(), zod4_using_value(), zod4_using_compiler(),])
diff --git a/test/index.ts b/test/index.ts
index a6814d5..2e56b20 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -5,3 +5,7 @@ import './typebox-from-zod'
import './typebox-from-valibot'
import './valibot-from-typebox'
import './zod-from-typebox'
+import './typebox-from-zod4'
+import './zod4-from-typebox'
+import './zod-detection-test'
+import './zod4-from-zod-and-zod-from-zod4'
diff --git a/test/typebox-from-zod.ts b/test/typebox-from-zod.ts
index ff76ec3..0e8dfa4 100644
--- a/test/typebox-from-zod.ts
+++ b/test/typebox-from-zod.ts
@@ -1,9 +1,9 @@
-import { TypeBox } from '@sinclair/typemap'
-import { TypeGuard } from '@sinclair/typebox'
+import { TypeBox, Compile } from '@sinclair/typemap'
+import { TypeGuard , TSchema} from '@sinclair/typebox'
import { Assert } from './assert'
import * as t from '@sinclair/typebox'
import * as z from 'zod'
-
+const ideally = process.env.EXTRA_TESTING === 'true' ? it : it.skip
describe('TypeBox From Zod', () => {
// ----------------------------------------------------------------
// Metadata
@@ -467,4 +467,274 @@ describe('TypeBox From Zod', () => {
const T = TypeBox(z.void())
Assert.IsTrue(TypeGuard.IsVoid(T))
})
+ // ----------------------------------------------------------------
+ // String Validation Behavior Tests
+ // ----------------------------------------------------------------
+
+ describe('String Validation Behavior', () => {
+ const Validator = (s: TSchema)=>{
+ const S = Compile(s);
+ return {
+ AssertValidateIsTrue: (v: string) => {
+ const result = S.Check(v);
+ const err = result ? "is-valid" : `Validation was false (expected true) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`;
+ Assert.IsEqual(err, "is-valid");
+ },
+ AssertValidateIsFalse: (v: string) => {
+ const result = S.Check(v);
+ const err = !result ? "not-valid" : `Validation was true (expected false) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`;
+ Assert.IsEqual(err, "not-valid");
+ }
+ }
+ }
+
+ // Tests for single string checks
+ describe('Basic String Checks', () => {
+ it('Should validate startsWith correctly', () => {
+ const schema = TypeBox(z.string().startsWith('prefix'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that start with 'prefix'
+ AssertValidateIsTrue('prefix123');
+ AssertValidateIsTrue('prefixABC');
+
+ // Should reject strings that don't start with 'prefix'
+ AssertValidateIsFalse('wrongprefix');
+ AssertValidateIsFalse('wrong');
+ });
+
+ it('Should validate endsWith correctly', () => {
+ const schema = TypeBox(z.string().endsWith('suffix'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that end with 'suffix'
+ AssertValidateIsTrue('123suffix');
+ AssertValidateIsTrue('ABCsuffix');
+
+ // Should reject strings that don't end with 'suffix'
+ AssertValidateIsFalse('suffixwrong');
+ AssertValidateIsFalse('wrong');
+ });
+ ideally('Should validate includes correctly', () => {
+ const schema = TypeBox(z.string().includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that include 'middle'
+ AssertValidateIsTrue('start-middle-end');
+ AssertValidateIsTrue('middleAtStart');
+ AssertValidateIsTrue('atTheMiddle');
+
+ // Should reject strings that don't include 'middle'
+ AssertValidateIsFalse('nomid');
+ AssertValidateIsFalse('midle'); // Misspelled
+ });
+ ideally('Should validate regex patterns correctly', () => {
+ const schema = TypeBox(z.string().regex(/^A[BC]+D$/));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that match the regex
+ AssertValidateIsTrue('ABCD');
+ AssertValidateIsTrue('AABBBCD');
+ AssertValidateIsTrue('AABCD');
+ AssertValidateIsTrue('AABBBBCD');
+ // Should reject strings that don't match the regex
+ AssertValidateIsFalse('AC');
+ AssertValidateIsFalse('123');
+ AssertValidateIsFalse('mixed123');
+ });
+
+ // Tests for combined string checks
+ describe('Combined String Checks', () => {
+ it('Should validate startsWith + endsWith correctly', () => {
+ const schema = TypeBox(z.string().startsWith('start').endsWith('end'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that start with 'start' AND end with 'end'
+ AssertValidateIsTrue('startend');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('startonly');
+ AssertValidateIsFalse('endonly');
+ AssertValidateIsFalse('neither');
+ });
+ ideally('Should validate startsWith + includes correctly', () => {
+ const schema = TypeBox(z.string().startsWith('start').includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that start with 'start' AND include 'middle'
+ AssertValidateIsTrue('startmiddle');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('startonly');
+ AssertValidateIsFalse('middleonly');
+ AssertValidateIsFalse('neither');
+ });
+ ideally('Should validate endsWith + includes correctly', () => {
+ const schema = TypeBox(z.string().endsWith('end').includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that end with 'end' AND include 'middle'
+ AssertValidateIsTrue('middleend');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('endonly');
+ AssertValidateIsFalse('middleonly');
+ AssertValidateIsFalse('neither');
+ });
+ ideally('Should validate startsWith + endsWith + includes correctly', () => {
+ const schema = TypeBox(z.string().startsWith('start').endsWith('end').includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that meet all three conditions
+ AssertValidateIsTrue('startmiddleend');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet all conditions
+ AssertValidateIsFalse('startend'); // Missing 'middle'
+ AssertValidateIsFalse('startmiddle'); // Missing 'end'
+ AssertValidateIsFalse('middlewithend'); // Missing 'start'
+ AssertValidateIsFalse('neither');
+ });
+ });
+
+ // Tests for special cases mentioned in the comments
+ describe('Special Case Combinations', () => {
+ it('Should validate ipv4 with startsWith correctly', () => {
+ // This tests the commented case "z.ipv4().startsWith('192.168.')"
+ const schema = TypeBox(z.string().ip("v4").startsWith('192.168.'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept IPv4 addresses that start with '192.168.'
+ AssertValidateIsTrue('192.168.1.1');
+ AssertValidateIsTrue('192.168.0.1');
+ AssertValidateIsTrue('192.168.254.254');
+
+ // Should reject IPv4 addresses that don't start with '192.168.'
+ AssertValidateIsFalse('10.0.0.1');
+ AssertValidateIsFalse('172.16.0.1');
+
+ // Should reject strings that start with '192.168.' but aren't valid IPv4s
+ AssertValidateIsFalse('192.168.');
+ AssertValidateIsFalse('192.168.1');
+ AssertValidateIsFalse('192.168.1.');
+ AssertValidateIsFalse('192.168.256.1');
+ });
+ ideally('Should validate regex with startsWith correctly', () => {
+ const schema = TypeBox(z.string().regex(/^[a-z]+$/).startsWith('abc'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that are lowercase AND start with 'abc'
+ AssertValidateIsTrue('abc');
+ AssertValidateIsTrue('abcdef');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('abcDEF'); // Not all lowercase
+ AssertValidateIsFalse('def'); // Doesn't start with 'abc'
+ });
+
+ it('Should validate email with endsWith correctly', () => {
+ const schema = TypeBox(z.string().email().endsWith('@example.com'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept emails that end with '@example.com'
+ AssertValidateIsTrue('user@example.com');
+ AssertValidateIsTrue('another.user@example.com');
+
+ // Should reject emails that don't end with '@example.com'
+ AssertValidateIsFalse('user@gmail.com');
+ AssertValidateIsFalse('user@example.org');
+
+ // Should reject invalid emails even if they end with '@example.com'
+ AssertValidateIsFalse('invalid@example.com@example.com');
+ AssertValidateIsFalse('@example.com');
+ });
+ });
+
+ // Edge cases
+ describe('Edge Cases', () => {
+ it('Should handle empty string checks correctly', () => {
+ const schema = TypeBox(z.string().startsWith('').endsWith(''));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Empty strings at start/end should match any string
+ AssertValidateIsTrue('anystring');
+ AssertValidateIsTrue('');
+ });
+ ideally('Should handle special regex characters in string checks', () => {
+ const schema = TypeBox(z.string().startsWith('$^.+*?()[]{}|\\'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should properly escape regex special chars
+ AssertValidateIsTrue('$^.+*?()[]{}|\\followed');
+ AssertValidateIsFalse('normal');
+ });
+ ideally('Should handle conflicting constraints', () => {
+ // This tests what happens with mutually exclusive constraints
+ const schema = TypeBox(z.string().startsWith('abc').startsWith('def'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // A string can't start with both 'abc' and 'def', so validation should fail
+ AssertValidateIsFalse('abcdef');
+ AssertValidateIsFalse('defabc');
+ });
+ });
+ });
+
+ // ----------------------------------------------------------------
+ // Format and Constraint Combinations
+ // ----------------------------------------------------------------
+ describe('Format and Constraint Combinations', () => {
+ it('Should combine format with length constraints', () => {
+ const schema = TypeBox(z.string().email().min(10).max(30));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Valid email within length constraints
+ AssertValidateIsTrue('test@example.com'); // 15 chars
+
+ // Valid email too short
+ AssertValidateIsFalse('a@b.com'); // 7 chars
+
+ // Valid email too long
+ AssertValidateIsFalse('verylongusername@verylongdomain.com'); // > 30 chars
+
+ // Invalid email but correct length
+ AssertValidateIsFalse('notanemailbutlongenough'); // 24 chars
+ });
+
+ it('Should combine format with pattern constraints', () => {
+ // Test a domain-specific email format: only gmail addresses
+ const schema = TypeBox(z.string().email().endsWith('@gmail.com'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Valid gmail addresses
+ AssertValidateIsTrue('user@gmail.com');
+ AssertValidateIsTrue('test.account@gmail.com');
+
+ // Valid email but not gmail
+ AssertValidateIsFalse('user@example.com');
+
+ // Invalid email with gmail ending
+ AssertValidateIsFalse('not-valid@example@gmail.com');
+ });
+ ideally('Should handle multiple constraints on formats', () => {
+ // IPv4 addresses in a specific range with specific constraints
+ const schema = TypeBox(z.string().ip('v4').startsWith('192.168.').includes('.100'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Valid IPv4 meeting both constraints
+ AssertValidateIsTrue('192.168.0.100');
+ AssertValidateIsTrue('192.168.100.101');
+
+ // Valid IPv4 but missing one constraint
+ AssertValidateIsFalse('192.168.0.1'); // Missing .100
+ AssertValidateIsFalse('10.0.0.100'); // Has .100 but wrong prefix
+
+ // Invalid IPv4
+ AssertValidateIsFalse('192.168.300.100'); // Invalid IPv4 format
+ });
+ });
+});
})
diff --git a/test/typebox-from-zod4.ts b/test/typebox-from-zod4.ts
new file mode 100644
index 0000000..76a3f80
--- /dev/null
+++ b/test/typebox-from-zod4.ts
@@ -0,0 +1,507 @@
+import { TypeBox, Compile } from '@sinclair/typemap'
+import { TSchema, TString, TypeGuard } from '@sinclair/typebox'
+import { Assert } from './assert'
+import { z } from 'zod/v4'
+const ideally = process.env.EXTRA_TESTING === 'true' ? it : it.skip
+describe('TypeBox From Zod4', () => {
+ // ----------------------------------------------------------------
+ // Metadata
+ // ----------------------------------------------------------------
+ it('Should map Description', () => {
+ const T = TypeBox(z.number().describe('a number'))
+ Assert.IsEqual(T.description, 'a number')
+ })
+
+ // ----------------------------------------------------------------
+ // Any
+ // ----------------------------------------------------------------
+ it('Should map Any', () => {
+ const T = TypeBox(z.any())
+ Assert.IsTrue(TypeGuard.IsAny(T))
+ })
+
+ // ----------------------------------------------------------------
+ // Array
+ // ----------------------------------------------------------------
+ it('Should map Array', () => {
+ const T = TypeBox(z.array(z.number()))
+ Assert.IsTrue(TypeGuard.IsArray(T))
+ Assert.IsTrue(TypeGuard.IsNumber(T.items))
+ })
+
+ // ----------------------------------------------------------------
+ // BigInt
+ // ----------------------------------------------------------------
+ it('Should map BigInt', () => {
+ const T = TypeBox(z.bigint())
+ Assert.IsTrue(TypeGuard.IsBigInt(T))
+ })
+
+ // ----------------------------------------------------------------
+ // Boolean
+ // ----------------------------------------------------------------
+ it('Should map Boolean', () => {
+ const T = TypeBox(z.boolean())
+ Assert.IsTrue(TypeGuard.IsBoolean(T))
+ })
+
+ // ----------------------------------------------------------------
+ // Number
+ // ----------------------------------------------------------------
+ it('Should map Number', () => {
+ const T = TypeBox(z.number())
+ Assert.IsTrue(TypeGuard.IsNumber(T))
+ })
+
+ // ----------------------------------------------------------------
+ // Object
+ // ----------------------------------------------------------------
+ it('Should map Object', () => {
+ const T = TypeBox(z.object({ x: z.number() }))
+ Assert.IsTrue(TypeGuard.IsObject(T))
+ Assert.IsTrue(TypeGuard.IsNumber(T.properties.x))
+ })
+
+ // ----------------------------------------------------------------
+ // String
+ // ----------------------------------------------------------------
+ it('Should map String', () => {
+ const T = TypeBox(z.string())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ })
+
+ // ----------------------------------------------------------------
+ // String Formats
+ // ----------------------------------------------------------------
+ it('Should map String Email format', () => {
+ const T = TypeBox(z.email())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'email')
+ })
+
+ it('Should map String UUID format', () => {
+ const T = TypeBox(z.uuid())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'uuid')
+ })
+
+ it('Should map String URL format', () => {
+ const T = TypeBox(z.url())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'url')
+ })
+
+ it('Should map String IPv4 format', () => {
+ const T = TypeBox(z.ipv4())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'ipv4')
+ })
+
+ it('Should map String IPv6 format', () => {
+ const T = TypeBox(z.ipv6())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'ipv6')
+ })
+
+ it('Should map String CUID format', () => {
+ const T = TypeBox(z.cuid())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'cuid')
+ })
+
+ it('Should map String CUID2 format', () => {
+ const T = TypeBox(z.cuid2())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'cuid2')
+ })
+
+ it('Should map String ULID format', () => {
+ const T = TypeBox(z.ulid())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'ulid')
+ })
+
+ it('Should map String Base64 format', () => {
+ const T = TypeBox(z.base64())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'base64')
+ })
+
+ it('Should map String Base64URL format', () => {
+ const T = TypeBox(z.base64url())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'base64url')
+ })
+
+ // ISO date formats
+ it('Should map String ISO Date format', () => {
+ const T = TypeBox(z.iso.date())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'date')
+ })
+
+ it('Should map String ISO DateTime format', () => {
+ const T = TypeBox(z.iso.datetime())
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'datetime')
+ })
+
+ // ----------------------------------------------------------------
+ // Edge Cases & Complex Cases
+ // ----------------------------------------------------------------
+ it('Should map string format validators with transform', () => {
+ const T = TypeBox(z.email().transform(val => val.toLowerCase()))
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'email')
+ })
+
+ it('Should map string format validators with pipe', () => {
+ const T = TypeBox(z.string().pipe(z.email()))
+ Assert.IsTrue(TypeGuard.IsString(T))
+ // This might actually fail depending on how pipe is implemented in Zod v4
+ })
+
+ it('Should map optional string format validators', () => {
+ const T = TypeBox(z.email().optional())
+ Assert.IsTrue(TypeGuard.IsOptional(T))
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.format, 'email')
+ })
+
+ it('Should map array of string format validators', () => {
+ const T = TypeBox(z.array(z.email()))
+ Assert.IsTrue(TypeGuard.IsArray(T))
+ Assert.IsTrue(TypeGuard.IsString(T.items))
+ Assert.IsEqual(T.items.format, 'email')
+ })
+
+ it('Should map object with string format validators', () => {
+ const T = TypeBox(z.object({
+ email: z.email(),
+ uuid: z.uuid(),
+ url: z.url()
+ }))
+ Assert.IsTrue(TypeGuard.IsObject(T))
+ Assert.IsTrue(TypeGuard.IsString(T.properties.email))
+ Assert.IsEqual(T.properties.email.format, 'email')
+ Assert.IsTrue(TypeGuard.IsString(T.properties.uuid))
+ Assert.IsEqual(T.properties.uuid.format, 'uuid')
+ Assert.IsTrue(TypeGuard.IsString(T.properties.url))
+ Assert.IsEqual(T.properties.url.format, 'url')
+ })
+ it("Should respect string validator chaining",()=>{
+ const T = TypeBox(z.email().min(5).max(10))
+ Assert.IsTrue(TypeGuard.IsString(T))
+ Assert.IsEqual(T.minLength, 5)
+ Assert.IsEqual(T.maxLength, 10)
+ Assert.IsEqual(T.format, 'email')
+ })
+})
+
+// ----------------------------------------------------------------
+// String Validation Behavior Tests
+// ----------------------------------------------------------------
+const Validator = (s: TSchema)=>{
+ const S = Compile(s);
+ return {
+ AssertValidateIsTrue: (v: string) => {
+ const result = S.Check(v);
+ const err = result ? "is-valid" : `Validation was false (expected true) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`;
+ Assert.IsEqual(err, "is-valid");
+ },
+ AssertValidateIsFalse: (v: string) => {
+ const result = S.Check(v);
+ const err = !result ? "not-valid" : `Validation was true (expected false) for value: '${v}' with schema:${JSON.stringify(s , null, 2)}`;
+ Assert.IsEqual(err, "not-valid");
+ }
+ }
+}
+describe('String Validation Behavior', () => {
+
+
+ // Tests for single string checks
+ describe('Basic String Checks', () => {
+ it('Should validate startsWith correctly', () => {
+ const schema = TypeBox(z.string().startsWith('prefix'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that start with 'prefix'
+ AssertValidateIsTrue('prefix123');
+ AssertValidateIsTrue('prefixABC');
+
+ // Should reject strings that don't start with 'prefix'
+ AssertValidateIsFalse('wrongprefix');
+ AssertValidateIsFalse('wrong');
+ });
+
+ it('Should validate endsWith correctly', () => {
+ const schema = TypeBox(z.string().endsWith('suffix'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that end with 'suffix'
+ AssertValidateIsTrue('123suffix');
+ AssertValidateIsTrue('ABCsuffix');
+
+ // Should reject strings that don't end with 'suffix'
+ AssertValidateIsFalse('suffixwrong');
+ AssertValidateIsFalse('wrong');
+ });
+
+ ideally('Should validate includes correctly', () => {
+ const schema = TypeBox(z.string().includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that include 'middle'
+ AssertValidateIsTrue('start-middle-end');
+ AssertValidateIsTrue('middleAtStart');
+ AssertValidateIsTrue('atTheMiddle');
+
+ // Should reject strings that don't include 'middle'
+ AssertValidateIsFalse('nomid');
+ AssertValidateIsFalse('midle'); // Misspelled
+ });
+
+ ideally('Should validate regex patterns correctly', () => {
+ const schema = TypeBox(z.string().regex(/^A[BC]+D$/));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that match the regex
+ AssertValidateIsTrue('ABCD');
+ AssertValidateIsTrue('AABBBCD');
+ AssertValidateIsTrue('AABCD');
+ AssertValidateIsTrue('AABBBBCD');
+ // Should reject strings that don't match the regex
+ AssertValidateIsFalse('AC');
+ AssertValidateIsFalse('123');
+ AssertValidateIsFalse('mixed123');
+ });
+
+ it('Should validate lowercase correctly', () => {
+ const schema = TypeBox(z.string().lowercase());
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept lowercase strings
+ AssertValidateIsTrue('abc');
+ AssertValidateIsTrue('lowercase');
+
+ // Should reject strings with uppercase
+ AssertValidateIsFalse('Abc');
+ AssertValidateIsFalse('UPPERCASE');
+ AssertValidateIsFalse('mixedCASE');
+ });
+
+ it('Should validate uppercase correctly', () => {
+ const schema = TypeBox(z.string().uppercase());
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept uppercase strings
+ AssertValidateIsTrue('ABC');
+ AssertValidateIsTrue('UPPERCASE');
+
+ // Should reject strings with lowercase
+ AssertValidateIsFalse('aBC');
+ AssertValidateIsFalse('lowercase');
+ AssertValidateIsFalse('mixedCASE');
+ });
+ });
+
+ // Tests for combined string checks
+ describe('Combined String Checks', () => {
+ it('Should validate startsWith + endsWith correctly', () => {
+ const schema = TypeBox(z.string().startsWith('start').endsWith('end'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that start with 'start' AND end with 'end'
+ AssertValidateIsTrue('startend');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('startonly');
+ AssertValidateIsFalse('endonly');
+ AssertValidateIsFalse('neither');
+ });
+
+ it('Should validate startsWith + includes correctly', () => {
+ const schema = TypeBox(z.string().startsWith('start').includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that start with 'start' AND include 'middle'
+ AssertValidateIsTrue('startmiddle');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('startonly');
+ AssertValidateIsFalse('middleonly');
+ AssertValidateIsFalse('neither');
+ });
+
+ it('Should validate endsWith + includes correctly', () => {
+ const schema = TypeBox(z.string().endsWith('end').includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that end with 'end' AND include 'middle'
+ AssertValidateIsTrue('middleend');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('endonly');
+ AssertValidateIsFalse('middleonly');
+ AssertValidateIsFalse('neither');
+ });
+
+ it('Should validate startsWith + endsWith + includes correctly', () => {
+ const schema = TypeBox(z.string().startsWith('start').endsWith('end').includes('middle'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that meet all three conditions
+ AssertValidateIsTrue('startmiddleend');
+ AssertValidateIsTrue('start_middle_end');
+
+ // Should reject strings that don't meet all conditions
+ AssertValidateIsFalse('startend'); // Missing 'middle'
+ AssertValidateIsFalse('startmiddle'); // Missing 'end'
+ AssertValidateIsFalse('middlewithend'); // Missing 'start'
+ AssertValidateIsFalse('neither');
+ });
+ });
+
+ // Tests for special cases mentioned in the comments
+ describe('Special Case Combinations', () => {
+ it('Should validate ipv4 with startsWith correctly', () => {
+ // This tests the commented case "z.ipv4().startsWith('192.168.')"
+ const schema = TypeBox(z.ipv4().startsWith('192.168.'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept IPv4 addresses that start with '192.168.'
+ AssertValidateIsTrue('192.168.1.1');
+ AssertValidateIsTrue('192.168.0.1');
+ AssertValidateIsTrue('192.168.254.254');
+
+ // Should reject IPv4 addresses that don't start with '192.168.'
+ AssertValidateIsFalse('10.0.0.1');
+ AssertValidateIsFalse('172.16.0.1');
+
+ // Should reject strings that start with '192.168.' but aren't valid IPv4s
+ AssertValidateIsFalse('192.168.');
+ AssertValidateIsFalse('192.168.1');
+ AssertValidateIsFalse('192.168.1.');
+ AssertValidateIsFalse('192.168.256.1');
+ });
+
+ ideally('Should validate regex with startsWith correctly', () => {
+ const schema = TypeBox(z.string().regex(/^[a-z]+$/).startsWith('abc'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept strings that are lowercase AND start with 'abc'
+ AssertValidateIsTrue('abc');
+ AssertValidateIsTrue('abcdef');
+
+ // Should reject strings that don't meet both conditions
+ AssertValidateIsFalse('abcDEF'); // Not all lowercase
+ AssertValidateIsFalse('def'); // Doesn't start with 'abc'
+ });
+
+ it('Should validate email with endsWith correctly', () => {
+ const schema = TypeBox(z.email().endsWith('@example.com'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should accept emails that end with '@example.com'
+ AssertValidateIsTrue('user@example.com');
+ AssertValidateIsTrue('another.user@example.com');
+
+ // Should reject emails that don't end with '@example.com'
+ AssertValidateIsFalse('user@gmail.com');
+ AssertValidateIsFalse('user@example.org');
+
+ // Should reject invalid emails even if they end with '@example.com'
+ AssertValidateIsFalse('invalid@example.com@example.com');
+ AssertValidateIsFalse('@example.com');
+ });
+ });
+
+ // Edge cases
+ describe('Edge Cases', () => {
+ it('Should handle empty string checks correctly', () => {
+ const schema = TypeBox(z.string().startsWith('').endsWith(''));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Empty strings at start/end should match any string
+ AssertValidateIsTrue('anystring');
+ AssertValidateIsTrue('');
+ });
+
+ it('Should handle special regex characters in string checks', () => {
+ const schema = TypeBox(z.string().startsWith('$^.+*?()[]{}|\\'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Should properly escape regex special chars
+ AssertValidateIsTrue('$^.+*?()[]{}|\\followed');
+ AssertValidateIsFalse('normal');
+ });
+
+ it('Should handle conflicting constraints', () => {
+ // This tests what happens with mutually exclusive constraints
+ const schema = TypeBox(z.string().startsWith('abc').startsWith('def'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // A string can't start with both 'abc' and 'def', so validation should fail
+ AssertValidateIsFalse('abcdef');
+ AssertValidateIsFalse('defabc');
+ });
+ });
+});
+
+// ----------------------------------------------------------------
+// Format and Constraint Combinations
+// ----------------------------------------------------------------
+describe('Format and Constraint Combinations', () => {
+ it('Should combine format with length constraints', () => {
+ const schema = TypeBox(z.email().min(10).max(30));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Valid email within length constraints
+ AssertValidateIsTrue('test@example.com'); // 15 chars
+
+ // Valid email too short
+ AssertValidateIsFalse('a@b.com'); // 7 chars
+
+ // Valid email too long
+ AssertValidateIsFalse('verylongusername@verylongdomain.com'); // > 30 chars
+
+ // Invalid email but correct length
+ AssertValidateIsFalse('notanemailbutlongenough'); // 24 chars
+ });
+
+ it('Should combine format with pattern constraints', () => {
+ // Test a domain-specific email format: only gmail addresses
+ const schema = TypeBox(z.email().endsWith('@gmail.com'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Valid gmail addresses
+ AssertValidateIsTrue('user@gmail.com');
+ AssertValidateIsTrue('test.account@gmail.com');
+
+ // Valid email but not gmail
+ AssertValidateIsFalse('user@example.com');
+
+ // Invalid email with gmail ending
+ AssertValidateIsFalse('not-valid@example@gmail.com');
+ });
+
+ it('Should handle multiple constraints on formats', () => {
+ // IPv4 addresses in a specific range with specific constraints
+ const schema = TypeBox(z.ipv4().startsWith('192.168.').includes('.100'));
+ const {AssertValidateIsTrue, AssertValidateIsFalse } = Validator(schema);
+
+ // Valid IPv4 meeting both constraints
+ AssertValidateIsTrue('192.168.0.100');
+ AssertValidateIsTrue('192.168.100.101');
+
+ // Valid IPv4 but missing one constraint
+ AssertValidateIsFalse('192.168.0.1'); // Missing .100
+ AssertValidateIsFalse('10.0.0.100'); // Has .100 but wrong prefix
+
+ // Invalid IPv4
+ AssertValidateIsFalse('192.168.300.100'); // Invalid IPv4 format
+ });
+});
diff --git a/test/zod-detection-test.ts b/test/zod-detection-test.ts
new file mode 100644
index 0000000..408a219
--- /dev/null
+++ b/test/zod-detection-test.ts
@@ -0,0 +1,24 @@
+import * as z3 from 'zod';
+import { z as z4 } from 'zod/v4';
+import { IsZod, IsZod4 } from '../src/guard';
+import { Assert } from './assert';
+
+describe('Zod Version Detection', () => {
+ // Create test schemas
+ const z3String = z3.string();
+ const z4String = z4.string();
+
+ describe('Zod v3 Detection', () => {
+ it('Should correctly identify Zod v3 schema', () => {
+ Assert.IsTrue(IsZod(z3String));
+ Assert.IsFalse(IsZod4(z3String));
+ });
+ });
+
+ describe('Zod v4 Detection', () => {
+ it('Should correctly identify Zod v4 schema', () => {
+ Assert.IsFalse(IsZod(z4String));
+ Assert.IsTrue(IsZod4(z4String));
+ });
+ });
+});
diff --git a/test/zod4-from-typebox.ts b/test/zod4-from-typebox.ts
new file mode 100644
index 0000000..55e3ec3
--- /dev/null
+++ b/test/zod4-from-typebox.ts
@@ -0,0 +1,61 @@
+import { Zod4 } from '@sinclair/typemap'
+import { Assert } from './assert'
+import * as t from '@sinclair/typebox'
+import { z } from 'zod/v4'
+
+describe('Zod4 From TypeBox', () => {
+ // ----------------------------------------------------------------
+ // Any
+ // ----------------------------------------------------------------
+ it('Should map Any', () => {
+ const Z = Zod4(t.Any())
+ Assert.IsTrue(Z.safeParse(1).success)
+ Assert.IsTrue(Z.safeParse('hello').success)
+ Assert.IsTrue(Z.safeParse(true).success)
+ })
+
+ // ----------------------------------------------------------------
+ // Boolean
+ // ----------------------------------------------------------------
+ it('Should map Boolean', () => {
+ const Z = Zod4(t.Boolean())
+ Assert.IsTrue(Z.safeParse(true).success)
+ Assert.IsFalse(Z.safeParse(1).success)
+ })
+
+ // ----------------------------------------------------------------
+ // Number
+ // ----------------------------------------------------------------
+ it('Should map Number', () => {
+ const Z = Zod4(t.Number())
+ Assert.IsTrue(Z.safeParse(1).success)
+ Assert.IsFalse(Z.safeParse('hello').success)
+ })
+
+ // ----------------------------------------------------------------
+ // Object
+ // ----------------------------------------------------------------
+ it('Should map Object', () => {
+ const Z = Zod4(t.Object({ x: t.Number(), y: t.String() }))
+ Assert.IsTrue(Z.safeParse({ x: 1, y: 'hello' }).success)
+ Assert.IsFalse(Z.safeParse({ x: 'hello', y: 1 }).success)
+ })
+
+ // ----------------------------------------------------------------
+ // String
+ // ----------------------------------------------------------------
+ it('Should map String', () => {
+ const Z = Zod4(t.String())
+ Assert.IsTrue(Z.safeParse('hello').success)
+ Assert.IsFalse(Z.safeParse(1).success)
+ })
+
+ // ----------------------------------------------------------------
+ // String Format
+ // ----------------------------------------------------------------
+ it('Should map String with Email format', () => {
+ const Z = Zod4(t.String({ format: 'email' }))
+ Assert.IsTrue(Z.safeParse('test@example.com').success)
+ Assert.IsFalse(Z.safeParse('not-an-email').success)
+ })
+})
diff --git a/test/zod4-from-zod-and-zod-from-zod4.ts b/test/zod4-from-zod-and-zod-from-zod4.ts
new file mode 100644
index 0000000..989b24d
--- /dev/null
+++ b/test/zod4-from-zod-and-zod-from-zod4.ts
@@ -0,0 +1,82 @@
+import { Zod, Zod4 } from '@sinclair/typemap'
+import { Assert } from './assert'
+import * as z from 'zod'
+import { z as z4 } from 'zod/v4'
+
+describe('Zod4 From Zod and Zod From Zod4', () => {
+ // ----------------------------------------------------------------
+ // String Conversion
+ // ----------------------------------------------------------------
+ it('Should convert Zod string to Zod4 string', () => {
+ const zodString = z.string()
+ const zod4String = Zod4(zodString)
+
+ // Verify it's a Zod4 string type
+ Assert.IsTrue(typeof zod4String.parse === 'function')
+ Assert.IsEqual(zod4String.parse('test'), 'test')
+ })
+
+ it('Should convert Zod4 string to Zod string', () => {
+ const zod4String = z4.string()
+ const zodString = Zod(zod4String)
+
+ // Verify it's a Zod string type
+ Assert.IsTrue(typeof zodString.parse === 'function')
+ Assert.IsEqual(zodString.parse('test'), 'test')
+ })
+
+ // ----------------------------------------------------------------
+ // Object Conversion
+ // ----------------------------------------------------------------
+ it('Should convert Zod object to Zod4 object', () => {
+ const zodObject = z.object({
+ name: z.string(),
+ age: z.number()
+ })
+
+ const zod4Object = Zod4(zodObject)
+
+ // Verify it's a Zod4 object type and can parse correctly
+ const testData = { name: 'John', age: 30 }
+ Assert.IsTrue(typeof zod4Object.parse === 'function')
+ const parsed = zod4Object.parse(testData)
+ Assert.IsEqual(parsed, testData)
+ })
+
+ it('Should convert Zod4 object to Zod object', () => {
+ const zod4Object = z4.object({
+ name: z4.string(),
+ age: z4.number()
+ })
+
+ const zodObject = Zod(zod4Object)
+
+ // Verify it's a Zod object type and can parse correctly
+ const testData = { name: 'John', age: 30 }
+ Assert.IsTrue(typeof zodObject.parse === 'function')
+ const parsed = zodObject.parse(testData)
+ Assert.IsEqual(parsed, testData)
+ })
+
+ // ----------------------------------------------------------------
+ // Back and Forth Conversion
+ // ----------------------------------------------------------------
+ it('Should preserve structure when converting Zod → Zod4 → Zod', () => {
+ const original = z.object({
+ name: z.string(),
+ age: z.number(),
+ isAdmin: z.boolean().optional()
+ })
+
+ const toZod4 = Zod4(original)
+ const backToZod = Zod(toZod4)
+
+ // Verify round-trip conversion works
+ const testData = { name: 'John', age: 30 }
+ Assert.IsEqual(backToZod.parse(testData), testData)
+
+ // Optional field should work too
+ const testDataWithOptional = { name: 'John', age: 30, isAdmin: true }
+ Assert.IsEqual(backToZod.parse(testDataWithOptional), testDataWithOptional)
+ })
+})
diff --git a/test/zod4-tsc-tests.ts b/test/zod4-tsc-tests.ts
new file mode 100644
index 0000000..0d7a6d2
--- /dev/null
+++ b/test/zod4-tsc-tests.ts
@@ -0,0 +1,625 @@
+import { TypeBoxFromZod4 as TypeBox } from '../src/typebox/typebox-from-zod4'
+import * as t from '@sinclair/typebox'
+import { z } from 'zod/v4'
+
+// Ensure that the TS types are assignable to what we expect
+type AssertEquivalence = T extends U ? (U extends T ? V : never) : never;
+
+// ------------------------------------------------------------------------
+// ZodAny
+// ------------------------------------------------------------------------
+// 1. Define a schema in Zod v4
+const anySchema = z.any();
+// 2. Manually define the expected output type
+type AnyType = t.TAny;
+// 3. Convert the Zod Schema into TypeBox schema
+const anyTypeBox = TypeBox(anySchema);
+// 4. Infer the type of the TypeBox type
+type AnyTypeBoxType = t.Static;
+// 5. Create a TS assertion
+const anyTypeAssertion: [
+ AssertEquivalence
+] = ["AnyTypeAssertion"];
+
+// ------------------------------------------------------------------------
+// ZodArray
+// ------------------------------------------------------------------------
+// 1. Define a schema in Zod v4
+const arraySchema = z.array(z.string());
+// 2. Manually define the expected output type
+type ArrayType = string[];
+// 3. Convert the Zod Schema into TypeBox schema
+const arrayTypeBox = TypeBox(arraySchema);
+// 4. Infer the type of the TypeBox type
+type ArrayTypeBoxType = t.Static;
+// 5. Create a TS assertion
+const arrayTypeAssertion: [
+ AssertEquivalence
+] = ["ArrayTypeAssertion"];
+
+// ------------------------------------------------------------------------
+// ZodBigInt
+// ------------------------------------------------------------------------
+// 1. Define a schema in Zod v4
+const bigIntSchema = z.bigint();
+// 2. Manually define the expected output type
+type BigIntType = bigint;
+// 3. Convert the Zod Schema into TypeBox schema
+const bigIntTypeBox = TypeBox(bigIntSchema);
+// 4. Infer the type of the TypeBox type
+type BigIntTypeBoxType = t.Static;
+// 5. Create a TS assertion
+const bigIntTypeAssertion: [
+ AssertEquivalence