
Security News
Attackers Are Hunting High-Impact Node.js Maintainers in a Coordinated Social Engineering Campaign
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.
zod-enum-forge
Advanced tools
Tiny helpers to extend Zod enums for open-set/iterative classification workflows.
Tiny helpers to extend Zod enums for open-set/iterative classification workflows.
zod-enum-forge provides utilities to dynamically extend Zod enums, making them "flexible" for scenarios where you need to handle unknown values in iterative data processing workflows, such as LLM-based classification tasks.
npm install zod-enum-forge
Requirements:
The library automatically detects and works with both Zod v3 and v4 - no configuration needed.
import { z } from 'zod';
import { flexEnum, forgeEnum } from 'zod-enum-forge';
// Create a flexible enum that can accept unknown values
const statusEnum = flexEnum(['pending', 'done']);
// Or from existing Zod enum
const baseEnum = z.enum(['a', 'b']);
const flexibleEnum = flexEnum(baseEnum);
// Extend an enum with new values
const extendedEnum = forgeEnum(['pending', 'done'], 'archived');
// Dynamic schema updates based on data
const schema = z.object({
status: flexEnum(['pending', 'done']),
category: z.enum(['urgent', 'normal'])
});
const data = {
status: 'in_progress', // New value!
category: 'urgent'
};
// Schema automatically extends to include new values
const updatedSchema = flexEnum(schema, data);
flexEnumCreates flexible enums that can accept unknown values and be dynamically extended based on data.
// Create from array of values
flexEnum(values: string[], description?: string): ZodUnion
// Create from existing ZodEnum
flexEnum(enumDef: ZodEnum, description?: string): ZodUnion
// Update schema based on data (auto-extends enums)
flexEnum(schema: ZodObject, dataJson: unknown): ZodObject
// Use specific Zod instance (for version control)
flexEnum(zodInstance: ZodType, values: string[], description?: string): ZodUnion
flexEnum(zodInstance: ZodType, enumDef: ZodEnum, description?: string): ZodUnion
import { z } from 'zod';
import { flexEnum } from 'zod-enum-forge';
// Basic flexible enum - accepts both predefined and unknown values
const statusEnum = flexEnum(['pending', 'done']);
console.log(statusEnum.parse('pending')); // ✅ 'pending'
console.log(statusEnum.parse('in_progress')); // ✅ 'in_progress' (unknown value accepted)
// With custom description for LLM guidance
const categoryEnum = flexEnum(['spam', 'ham'], 'Custom category type for email classification');
// Dynamic schema updates - automatically extends enums when new values are encountered
const schema = z.object({
status: flexEnum(['pending', 'done']),
category: flexEnum(['urgent', 'normal'])
});
const data = {
status: 'in_progress', // New value!
category: 'low_priority' // Another new value!
};
const updatedSchema = flexEnum(schema, data);
// Schema now accepts the new values for future validations
console.log(updatedSchema.parse(data)); // ✅ Works!
// Using specific Zod instance for version control
import { z as zod4 } from 'zod/v4';
const v4FlexEnum = flexEnum(zod4, ['a', 'b'], 'Custom description');
forgeEnumExtends existing enums with new values, creating a new enum with combined values.
// Extend array of values
forgeEnum(values: string[], add: string | string[]): ZodEnum
// Extend existing ZodEnum
forgeEnum(enumDef: ZodEnum, add: string | string[]): ZodEnum
// Extend enum within schema object
forgeEnum(schema: ZodObject, key: string, add: string | string[]): ZodObject
import { z } from 'zod';
import { forgeEnum } from 'zod-enum-forge';
// Extend array of values
const newEnum = forgeEnum(['a', 'b'], 'c');
// Result: enum with values ['a', 'b', 'c']
// Extend existing Zod enum
const baseEnum = z.enum(['pending', 'done']);
const extendedEnum = forgeEnum(baseEnum, ['archived', 'cancelled']);
// Result: enum with values ['pending', 'done', 'archived', 'cancelled']
// Extend enum within schema
const schema = z.object({
status: z.enum(['pending', 'done'])
});
const newSchema = forgeEnum(schema, 'status', 'archived');
// Schema now has status enum with 'archived' value
addToEnum (alias of forgeEnum)Alias that forwards to forgeEnum.
Signatures: same as forgeEnum.
Example:
import { addToEnum } from 'zod-enum-forge';
const base = z.enum(['a','b']);
const extended = addToEnum(base, 'c'); // enum with a,b,c
limitEnumRemove values from enums or flex enums. Supports:
Signatures:
limitEnum(values: string[], remove: string | string[]): ZodEnum
limitEnum(enumOrFlex: ZodEnum | ZodUnion /* flex */ , remove: string | string[]): ZodEnum | ZodUnion
limitEnum(schema: ZodObject, key: string, remove: string | string[]): ZodObject
Examples:
// Array
const reduced = limitEnum(['a','b','c'], 'b'); // enum a,c
// Enum
const Base = z.enum(['x','y','z']);
const trimmed = limitEnum(Base, ['z']); // enum x,y
// flexEnum
const fx = flexEnum(['draft','pub','arch']);
const fxTrimmed = limitEnum(fx, 'arch'); // flex without 'arch'
// In schema path
const schema = z.object({ status: z.enum(['open','closed','archived']) });
const updated = limitEnum(schema, 'status', 'archived');
deleteFromEnum (alias of limitEnum)Same usage as limitEnum.
strictEnumConvert a flex enum (or entire structure) back to a strict z.enum(...) removing the string-union flexibility and metadata. Preserves optional / nullable wrappers.
Signatures:
strictEnum(flexEnumOrEnum: ZodUnion | ZodEnum): ZodEnum
strictEnum(schema: ZodObject): ZodObject // cleans all nested flex enums
Example:
const fx = flexEnum(['a','b']);
const strict = strictEnum(fx); // plain z.enum(['a','b'])
const schema = z.object({ role: flexEnum(['admin','user']) });
const cleaned = strictEnum(schema); // role is now pure enum
deflexStructureAlias behaving like strictEnum when passed a structure. (You can still pass a single flex enum.)
deflexStructure(schema: ZodObject): ZodObject
isFlexEnumPredicate that returns true if the value is a flex enum (either union or plain enum with metadata).
isFlexEnum(x: unknown): boolean
Example:
const fx = flexEnum(['a','b']);
console.log(isFlexEnum(fx)); // true
separateFlexibilityRemoves flex enums from a structure (turns them into strict enums) and returns a layer describing original flexibility for later reintegration.
Signature:
separateFlexibility(schema: ZodObject): { schema: ZodObject; flexityLayer: FlexityLayer }
FlexityLayer shape:
type FlexityLayer = {
[path: string]: { values: string[]; description?: string }
};
Example:
const schema = z.object({ status: flexEnum(['pending','done']), nested: z.object({ kind: flexEnum(['a','b']).optional().nullable() }) });
const { schema: strictSchema, flexityLayer } = separateFlexibility(schema);
// strictSchema: all flex removed
// flexityLayer: { 'status': { values:['pending','done'], description: ... }, 'nested.kind': {...} }
integrateFlexibilityGiven a strict schema and a FlexityLayer, recreate the flex enums (original flexible form).
Signature:
integrateFlexibility(schema: ZodObject, flexityLayer: FlexityLayer): ZodObject
Example:
const { schema: strictSchema, flexityLayer } = separateFlexibility(schemaWithFlex);
const restored = integrateFlexibility(strictSchema, flexityLayer);
Added helpers for managing enum evolution lifecycle:
addToEnum: alias of forgeEnumlimitEnum: remove values from enums / flexEnums (arrays, direct enums, wrapped optional/nullable, schema paths, or flexEnum unions)deleteFromEnum: alias of limitEnumstrictEnum: converts flexEnum (union) or structures back to plain z.enum(...) (preserving optional/nullable wrappers) and removes metadatadeflexStructure: alias behaving like strictEnum on whole structuresisFlexEnum: exported predicate detecting flex enumsseparateFlexibility: returns { schema, flexityLayer } where schema has all flexEnums converted to strict enums and flexityLayer maps paths to original values & descriptionsintegrateFlexibility: given a strict schema and a flexityLayer recreates the original schema with flexEnums reintegratedFlexityLayer is a simple object: { 'path.to.field': { values: string[], description?: string } }
import { z } from 'zod';
import { flexEnum, separateFlexibility, integrateFlexibility, limitEnum, addToEnum, strictEnum } from 'zod-enum-forge';
const schema = z.object({
status: flexEnum(['pending','done']),
nested: z.object({ kind: flexEnum(['a','b']).optional().nullable() })
});
// Extract flexibility layer
const { schema: strictSchema, flexityLayer } = separateFlexibility(schema);
// strictSchema now contains pure enums
// flexityLayer records where flex enums were
// Reintegrate later
const restored = integrateFlexibility(strictSchema, flexityLayer);
// Remove a value from an enum
const trimmed = limitEnum(restored, 'status', 'done');
// Add a new value
const extended = addToEnum(trimmed, 'status', 'archived');
// Force convert a specific field back to strict
const strictAgain = strictEnum(extended.shape.status);
The library handles complex nested structures:
const schema = z.object({
textClassification: z.object({
category: flexEnum(['spam', 'ham']),
subCategory: flexEnum(['urgent', 'non-urgent']).optional().nullable(),
features: z.object({
sentiment: flexEnum(['positive', 'negative']),
intent: flexEnum(['inform', 'request', 'command'])
})
}),
metadata: z.object({
source: flexEnum(['email', 'chat'])
})
});
const newData = {
textClassification: {
category: 'offers', // New category
subCategory: 'urgent',
features: {
sentiment: 'neutral', // New sentiment
intent: 'inform'
}
},
metadata: {
source: 'sms' // New source
}
};
const updatedSchema = flexEnum(schema, newData);
// All new enum values are now supported
The library properly handles optional and nullable fields:
const schema = z.object({
required: flexEnum(['a', 'b']),
optional: flexEnum(['x', 'y']).optional().nullable()
});
const data = {
required: 'c', // Extends required field
optional: 'z' // Extends optional field (remains optional and nullable)
};
const updated = flexEnum(schema, data);
Perfect for iterative classification workflows where you discover new categories as you process data:
This example demonstrates a real-world scenario where you're processing Wikipedia articles with an LLM (GPT-4o) and discovering new classification categories on the fly. The schema starts with basic categories but grows automatically as the LLM encounters new types of content that don't fit existing categories.
How it works:
flexEnum automatically extends the schemaimport fs from "fs";
import OpenAI from "openai";
import { zodTextFormat } from "openai/helpers/zod";
import { z } from "zod";
import csv from 'async-csv';
import { flexEnum } from 'zod-enum-forge';
import 'dotenv/config';
// Classification schema for Wikipedia articles
const articleSchema = z.object({
textClassification: z.object({
category: flexEnum(['politics', 'mathematics', 'ecology']),
subCategory: flexEnum(['international politics', 'geometry', 'climate change']).optional().nullable(),
}),
keyfindings: z.object({
summary: z.string().max(500),
importantFigures: z.array(z.string()).min(1).max(5),
relatedArticles: z.array(z.string()).min(1).max(5)
})
});
async function main() {
let currArticleSchema = articleSchema;
// Load articles from CSV file
const articlesContent_raw = await fs.promises.readFile('./articles.csv', 'utf8');
const articlesContent = await csv.parse(articlesContent_raw) as string[][];
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
const articles = [];
// Process first 4 articles (skipping header row)
for (let n = 1; n < 5; n++) {
if (!articlesContent[n]?.[0]) {
continue; // Skip empty rows
}
const response = await openai.responses.parse({
model: "gpt-4o",
input: [
{ role: "system", content: "Write the information about article." },
{
role: "user",
content: "Article content:\n" + (articlesContent[n]?.[0] ?? ''),
},
],
text: {
format: zodTextFormat(currArticleSchema, "article"),
},
});
const article = response.output_parsed;
// Dynamically extend schema based on LLM output
currArticleSchema = flexEnum(currArticleSchema, article);
articles.push(article);
}
// Save processed articles and final schema
fs.writeFileSync('./processed_articles.json', JSON.stringify(articles, null, 2));
fs.writeFileSync('./last_schema.json', JSON.stringify(zodTextFormat(currArticleSchema, "article"), null, 2));
}
main().catch(err => {
console.error(err);
process.exit(1);
});
Key Benefits:
This approach is particularly useful for:
Build evolving taxonomies that grow with your data:
let taxonomy = z.object({
domain: flexEnum(['technology', 'business']),
subdomain: flexEnum(['ai', 'blockchain']).optional().nullable()
});
// As you process more documents
const documents = [
{ domain: 'healthcare', subdomain: 'telemedicine' },
{ domain: 'technology', subdomain: 'quantum' },
{ domain: 'business', subdomain: null } // nullable value
];
documents.forEach(doc => {
taxonomy = flexEnum(taxonomy, doc);
});
// Taxonomy now includes all discovered categories
This library (v0.2.0) automatically detects and works with both Zod v3 and v4:
_def property structure_zod.def property structure with traitsThe compatibility layer automatically:
Version Detection:
// Library automatically detects version from your schemas
const v3Schema = z3.enum(['a', 'b']);
const v4Schema = z4.enum(['a', 'b']);
// Both work seamlessly
const flexV3 = flexEnum(v3Schema);
const flexV4 = flexEnum(v4Schema);
// You can also specify the Zod instance explicitly
const explicitV4 = flexEnum(z4, ['a', 'b']);
No configuration needed - the library handles all differences internally.
flexEnum creates a Zod union type that combines:
// flexEnum(['a', 'b']) internally creates:
z.enum(['a', 'b']).or(z.string().describe("If none of the existing enum values match, provide a new appropriate value for this field."))
This approach provides:
When using flexEnum(schema, data):
const schema = z.object({
status: flexEnum(['pending', 'done']).optional(),
nested: z.object({
category: flexEnum(['a', 'b'])
})
});
const data = {
status: 'in_progress', // New value
nested: { category: 'c' } // New nested value
};
// Result: schema with extended enums, status remains optional
const newSchema = flexEnum(schema, data);
Full TypeScript support with proper type inference:
const schema = z.object({
status: flexEnum(['pending', 'done'])
});
type SchemaType = z.infer<typeof schema>;
// Result: { status: "pending" | "done" | string }
// The union type allows both predefined and custom values
const validData1: SchemaType = { status: 'pending' }; // ✅ Known value
const validData2: SchemaType = { status: 'custom' }; // ✅ Unknown value
The library provides clear error messages:
const schema = z.object({
name: z.string() // Not an enum
});
// This will throw: 'Field "name" is not a ZodEnum.'
forgeEnum(schema, 'name', 'test');
Source code is available on GitHub: itsp-kybernetes/zod-enum-forge
Contributions are welcome! Please feel free to submit a Pull Request.
FreeBSD-2-Clause © Mariusz Żabiński (kybernetes.ngo)
FAQs
Tiny helpers to extend Zod enums for open-set/iterative classification workflows.
We found that zod-enum-forge demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Multiple high-impact npm maintainers confirm they have been targeted in the same social engineering campaign that compromised Axios.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.