Zod 12 🛡️ Enums and Literals — z.enum() and z.literal() in v4
Zod provides two schemas for constraining a value to a fixed set: z.literal() for a single exact value and z.enum() for a set of string values. In Zod v4, the two constructors changed in ways that affect migration from v3. z.literal() dropped symbol support, and z.enum() absorbed the functionality of the deprecated z.nativeEnum(), now accepting TypeScript enums directly . The inferred types remain precise literal unions, which is what makes these schemas useful for discriminated unions and exhaustive switches.
This chapter covers the literal schema, the unified z.enum() constructor, and the v4 changes that determine which syntax is current. It explains why z.enum() is preferred over a union of literals for string sets, how the .enum property exposes the values, and what the z.nativeEnum() deprecation means for existing code. The sections that follow walk through literals, enums, and the migration patterns.
Key point: z.literal(v) accepts exactly one value; z.enum([...]) accepts a set of string literals and, in v4, also accepts TypeScript enums directly—z.nativeEnum() is deprecated.
Why literals and enums exist
The plain-string problem. A schema that uses z.string() for a status field accepts every string, including typos. z.enum(["pending", "shipped"]) accepts only the two valid values, and the inferred type is the literal union "pending" | "shipped". The compiler and the validator agree on the set, which is the point of using the schema instead of a bare string .
The single-value problem. Some fields are not a set but a single fixed value: a discriminator that is always "success", a version that is always 1, a type that is always "customer". z.literal("success") accepts only that value and infers the literal type. This is the schema used inside discriminated unions, where each branch has a distinct literal that the parser reads to select the branch .
The native-enum problem. Zod v3 had two constructors: z.enum() for string arrays and z.nativeEnum() for TypeScript/JavaScript enums. Zod v4 unified them. z.enum() now accepts a string array, a TypeScript enum (string or numeric), or an enum-like object. z.nativeEnum() still runs but is deprecated, and its type definition marks it as such . A codebase migrating to v4 should replace every z.nativeEnum(E) with z.enum(E).
a. z.literal() and the v4 changes
z.literal(value) creates a schema that accepts exactly one value. The inferred type is the literal type, not the broader primitive type.
import { z } from "zod";
const SuccessSchema = z.literal("success"); // "success"
const VersionSchema = z.literal(1); // 1
const TrueSchema = z.literal(true); // true
The v4 changes to z.literal() are narrow. Symbol support was removed because symbols are not literal values in the sense the constructor implies and cannot be compared with === in a type-safe way . The null and undefined values were already not supported in v3 and remain unsupported in v4; z.null() and z.undefined() are the correct schemas for those.
A literal is rarely used alone. Its most important role is inside a discriminated union, where it identifies the branch. The parser reads the literal field, finds the matching branch, and validates only that branch.
const ResponseSchema = z.discriminatedUnion("status", [
z.object({ status: z.literal("success"), data: z.unknown() }),
z.object({ status: z.literal("error"), message: z.string() }),
]);
b. z.enum() and the unified constructor
z.enum() accepts a set of values and infers the union of those values as literal types. In v4, it accepts three forms: a string array, a TypeScript enum, and an enum-like object.
// String array (the common case)
const StatusSchema = z.enum(["pending", "processing", "shipped"]);
// "pending" | "processing" | "shipped"
// TypeScript enum (v4 unified; was z.nativeEnum in v3)
enum Role {
Admin = "admin",
User = "user",
}
const RoleSchema = z.enum(Role);
// "admin" | "user"
The .enum property exposes the values as an object, and .options returns the values as an array. In v3, .Enum and .Values served these purposes; both are removed in v4, and .enum is the replacement . The exclude and extract methods derive new schemas from an existing enum schema.
StatusSchema.enum.pending; // "pending"
StatusSchema.options; // ["pending", "processing", "shipped"]
const OpenStatus = StatusSchema.exclude(["shipped"]); // pending | processing
The v4.6 change to numeric enums is worth noting. For a numeric TypeScript enum like enum Country { UK, Germany }, z.enum(Country).options now lists only the values ([0, 1]), without the reverse-mapping keys that TypeScript generates for numeric enums . This is a correctness fix: the reverse mappings are not valid enum values, and listing them as options was misleading.
c. Literals versus enums and the migration
The choice between z.literal() and z.enum() is determined by the size of the set. A single value uses z.literal(). A set of two or more string values uses z.enum(), which is more direct than a union of literals and produces the same inferred type .
// Preferred for a set
const SizeSchema = z.enum(["small", "medium", "large"]);
// Equivalent but more verbose
const SizeUnion = z.union([
z.literal("small"),
z.literal("medium"),
z.literal("large"),
]);
The migration from v3 to v4 is mechanical for enums. Replace z.nativeEnum(E) with z.enum(E). Replace .Enum with .enum. Replace .Values with .options. The ESLint rule zod/no-native-enum automates the first replacement when the schema is accessed through the z namespace .
The migration for literals is smaller. No changes are required unless the code used z.literal(Symbol()), which was removed. The null and undefined cases, if they existed, were already using z.null() and z.undefined().
Complete Example Session
// ============================================
// PART 1: LITERALS
// ============================================
import { z } from "zod";
const Success = z.literal("success");
const Version = z.literal(1);
Success.parse("success"); // valid
Success.parse("error"); // ZodError
// ============================================
// PART 2: STRING ARRAY ENUM
// ============================================
const Status = z.enum(["pending", "shipped"]);
Status.parse("pending"); // valid
Status.parse("unknown"); // ZodError
// ============================================
// PART 3: TYPESCRIPT ENUM (v4 UNIFIED)
// ============================================
enum Role {
Admin = "admin",
User = "user",
}
const RoleSchema = z.enum(Role); // was z.nativeEnum(Role) in v3
RoleSchema.parse("admin"); // valid
// ============================================
// PART 4: THE .enum PROPERTY
// ============================================
Status.enum.pending; // "pending"
Status.options; // ["pending", "shipped"]
// ============================================
// PART 5: DERIVED SCHEMAS
// ============================================
const OpenStatus = Status.exclude(["shipped"]); // "pending"
// ============================================
// PART 6: DISCRIMINATED UNION WITH LITERALS
// ============================================
const Event = z.discriminatedUnion("type", [
z.object({ type: z.literal("created"), id: z.string() }),
z.object({ type: z.literal("deleted"), id: z.string() }),
]);
The parts covered literals, string-array enums, TypeScript enums through the unified constructor, the .enum property, derived schemas, and the discriminated union that uses literals.
Quick Reference
Constructors
| Schema | Accepts | Inferred Type |
|---|---|---|
z.literal(v) | Exactly v | v |
z.enum([...]) | One of the listed strings | Union of literals |
z.enum(TSEnum) | One of the enum values | Union of enum values |
v3 to v4 Migration
| v3 Syntax | v4 Syntax |
|---|---|
z.nativeEnum(E) | z.enum(E) (deprecated in v4) |
.Enum | .enum |
.Values | .options |
Properties and Methods
| Property/Method | Returns |
|---|---|
.enum | Object of values |
.options | Array of values |
.exclude([...]) | Schema without listed values |
.extract([...]) | Schema with only listed values |
Best Practices
✅ Do This:
// Use literal for a single fixed value
z.literal("success") // ✅
// Use enum for a set of strings
z.enum(["pending", "shipped"]) // ✅
// Use the unified constructor for TS enums
z.enum(Role) // ✅
// Access values through .enum
Status.enum.pending // ✅
❌ Don’t Do This:
// Use z.nativeEnum (deprecated)
z.nativeEnum(Role) // ❌
// Use a plain string for a fixed set
z.string() // accepts typos // ❌
// Use a union of literals for a set
z.union([z.literal("a"), z.literal("b")]) // ❌ (use z.enum)
// Expect .Enum or .Values
Status.Enum // removed in v4 // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
z.nativeEnum not a function | Removed in v4 | Use z.enum(E) |
.Enum undefined | Removed in v4 | Use .enum |
.Values undefined | Removed in v4 | Use .options |
| Numeric enum options include keys | Fixed in v4.6 | Upgrade |
Real-World Examples
1. Single literal
z.literal("success");
2. Numeric literal
z.literal(200);
3. String enum
z.enum(["small", "medium", "large"]);
4. TypeScript enum (v4)
z.enum(Role);
5. Access values
Status.enum.pending;
6. Options array
Status.options;
7. Exclude a value
Status.exclude(["shipped"]);
8. Literal in discriminated union
type: z.literal("created");
9. ESLint autofix
zod/no-native-enum converts z.nativeEnum to z.enum
10. Numeric enum options (v4.6)
// z.enum(Country).options === [0, 1]
Visual
Literal vs Enum
┌─────────────────────────────────────────────────────────┐
│ z.literal("success") │
│ ┌──────────────────────────────────────────┐ │
│ │ Accepts: "success" only │ │
│ │ Type: "success" │ │
│ └──────────────────────────────────────────┘ │
│ │
│ z.enum(["pending", "shipped"]) │
│ ┌──────────────────────────────────────────┐ │
│ │ Accepts: "pending" or "shipped" │ │
│ │ Type: "pending" | "shipped" │ │
│ └──────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────┘
v4 Enum Unification
┌─────────────────────────────────────────────────────────┐
│ v3 v4 │
│ │
│ z.enum(["a", "b"]) z.enum(["a", "b"]) │
│ z.nativeEnum(MyEnum) z.enum(MyEnum) │
│ (nativeEnum deprecated) │
│ │
│ .Enum .enum │
│ .Values .options │
└─────────────────────────────────────────────────────────┘
Enum Properties
┌─────────────────────────────────────────────────────────┐
│ const Status = z.enum(["pending", "shipped"]) │
│ │
│ Status.enum → { pending: "pending", │
│ shipped: "shipped" } │
│ Status.options → ["pending", "shipped"] │
│ Status.exclude(["shipped"]) → z.enum(["pending"]) │
└─────────────────────────────────────────────────────────┘
Literal in Discriminated Union
┌─────────────────────────────────────────────────────────┐
│ type: z.literal("created") │
│ type: z.literal("deleted") │
│ │
│ z.discriminatedUnion("type", [...]) │
│ │ │
│ ▼ │
│ Parser reads "type", dispatches to matching branch │
│ │
│ The literal is the dispatch key │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Single value | z.literal(v) |
| String set | z.enum([...]) |
| TypeScript enum (v4) | z.enum(TSEnum) |
| Deprecated | z.nativeEnum() |
| Values object | .enum |
| Values array | .options |
| Derived schema | .exclude() / .extract() |
| v4 literal change | symbol support removed |
Key takeaways:
- Literals are for single values; enums are for sets.
z.literal("success")accepts one exact value.z.enum([...])accepts a set of strings and infers the union of those literals . - v4 unified
z.enum(). It now accepts TypeScript enums directly, andz.nativeEnum()is deprecated. The migration isz.nativeEnum(E)→z.enum(E). - The
.enumand.optionsproperties replaced.Enumand.Values..enumreturns the values as an object;.optionsreturns them as an array. Both v3 properties are removed . z.literal()dropped symbol support in v4. Symbols are not literal values in the sense the constructor implies. Null and undefined were already unsupported; usez.null()andz.undefined().- Enums are the correct schema for fixed string sets. A plain
z.string()accepts typos;z.enum()catches them at the boundary and gives the compiler the literal union to narrow on .
Remember: A literal is a single value; an enum is a set. Use z.literal() for discriminators and constants, and z.enum() for any field that should accept one of several named values. In v4, the enum constructor is unified: it takes string arrays, TypeScript enums, and enum-like objects. If the code still calls z.nativeEnum(), it is calling a deprecated function that will eventually be removed. The migration is mechanical, and the ESLint rule can automate it.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!