Schema Validation Modes
@apical-ts/craft provides fine-grained control over how generated Zod schemas
handle additional properties through the --extra-props CLI flag. This feature
allows you to configure validation behavior for object schemas based on your
application's requirements.
Overview
By default, Zod objects will parse successfully even when additional properties
are present in the input data, but these extra properties are stripped from the
output. The --extra-props flag allows you to change this behavior to either be
more permissive (loose mode) or more restrictive (strict mode).
CLI Usage
Add the --extra-props flag to your generate command:
npx @apical-ts/craft generate \
--extra-props <mode> \
-i openapi.yaml \
-o generated
Where <mode> can be one of:
strip(default) - Additional properties are removed from parsed objectsloose- Additional properties are preserved in parsed objectsstrict- Additional properties cause validation errors
Validation Modes
Strip Mode (Default)
When --extra-props is not specified or set to strip, schemas behave like
standard Zod objects:
# These commands are equivalent
npx @apical-ts/craft generate -i openapi.yaml -o generated
npx @apical-ts/craft generate --extra-props strip -i openapi.yaml -o generated
Generated schema:
export const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
});
Behavior:
const input = {
id: 1,
name: "John",
email: "john@example.com",
extraField: "ignored",
};
const result = UserSchema.parse(input);
// result = { id: 1, name: "John", email: "john@example.com" }
// extraField is stripped away
Loose Mode
When set to loose, additional properties are preserved in the parsed result:
npx @apical-ts/craft generate --extra-props loose -i openapi.yaml -o generated
Generated schema:
export const UserSchema = z
.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
})
.loose();
Behavior:
const input = {
id: 1,
name: "John",
email: "john@example.com",
extraField: "preserved",
};
const result = UserSchema.parse(input);
// result = { id: 1, name: "John", email: "john@example.com", extraField: "preserved" }
// extraField is kept in the result
Strict Mode
When set to strict, additional properties cause validation to fail:
npx @apical-ts/craft generate --extra-props strict -i openapi.yaml -o generated
Generated schema:
export const UserSchema = z
.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
})
.strict();
Behavior:
const input = {
id: 1,
name: "John",
email: "john@example.com",
extraField: "causes_error",
};
const result = UserSchema.safeParse(input);
// result.success = false
// result.error contains details about the unexpected property
OpenAPI additionalProperties Handling
The --extra-props flag only affects object schemas that do not have an
explicit additionalProperties setting in the OpenAPI specification. When
additionalProperties is explicitly defined, it takes precedence:
Explicit additionalProperties: false
# OpenAPI Schema
User:
type: object
additionalProperties: false
properties:
id:
type: integer
name:
type: string
Generated schema (regardless of --extra-props flag):
export const UserSchema = z
.object({
id: z.number(),
name: z.string(),
})
.strict();
The schema is always strict when additionalProperties: false is explicitly
set.
Explicit additionalProperties: true
# OpenAPI Schema
User:
type: object
additionalProperties: true
properties:
id:
type: integer
name:
type: string
Generated schema (regardless of --extra-props flag):
export const UserSchema = z
.object({
id: z.number(),
name: z.string(),
})
.catchall(z.unknown());
The .catchall(z.unknown()) method allows any additional properties to be
present and preserved in the parsed result.
additionalProperties with Schema
# OpenAPI Schema
User:
type: object
additionalProperties:
type: string
properties:
id:
type: integer
name:
type: string
Generated schema (regardless of --extra-props flag):
export const UserSchema = z
.object({
id: z.number(),
name: z.string(),
})
.catchall(z.string());
When additionalProperties contains a schema definition, Zod's .catchall()
method is used to validate additional properties according to the specified
schema.
Behavior:
const input = {
id: 1,
name: "John",
email: "john@example.com", // string - valid
age: 25, // number - invalid, should be string
};
const result = UserSchema.safeParse(input);
// result.success = false (age is not a string)
// Only additional properties matching the schema type are allowed
additionalProperties with Complex Schema
# OpenAPI Schema
User:
type: object
additionalProperties:
type: object
properties:
value:
type: string
metadata:
type: object
properties:
id:
type: integer
name:
type: string
Generated schema:
export const UserSchema = z
.object({
id: z.number(),
name: z.string(),
})
.catchall(
z.object({
value: z.string(),
metadata: z.object({}).passthrough(),
}),
);
additionalProperties: true vs No Explicit Setting
With additionalProperties: true:
User:
type: object
additionalProperties: true # Explicitly allows any additional properties
properties:
id:
type: integer
Generated schema:
export const UserSchema = z
.object({
id: z.number(),
})
.catchall(z.unknown());
Without explicit additionalProperties (subject to --extra-props):
User:
type: object
# No additionalProperties specified
properties:
id:
type: integer
Generated schema (depends on --extra-props flag):
// --extra-props strip (default)
export const UserSchema = z.object({
id: z.number(),
});
// --extra-props loose
export const UserSchema = z
.object({
id: z.number(),
})
.loose();
// --extra-props strict
export const UserSchema = z
.object({
id: z.number(),
})
.strict();
Use Cases
Strip Mode (Default)
- Best for: Most applications where you want clean, predictable data structures
- Use when: You want to ignore unknown fields from API responses
- Security: Prevents unexpected data from reaching your application logic
Loose Mode
- Best for: Working with evolving APIs or third-party services
- Use when: You need to preserve all data from API responses for logging or debugging
- Flexibility: Allows handling of API versions with additional fields
Strict Mode
- Best for: Critical applications requiring exact data validation
- Use when: You want to catch API contract violations immediately
- Reliability: Ensures your application only processes expected data structures
Examples
Basic Usage
Generate schemas with strict validation:
npx @apical-ts/craft generate \
--extra-props strict \
--client \
-i https://api.example.com/openapi.json \
-o ./generated
Combined with Other Options
Use loose mode with server generation:
npx @apical-ts/craft generate \
--extra-props loose \
--server \
--client \
-i ./specs/api.yaml \
-o ./src/generated
Runtime Usage
After generation, use the schemas in your application:
import { UserSchema } from "./generated/schemas";
// Strict validation example
try {
const user = UserSchema.parse(apiResponse);
// Process user data knowing it's exactly what's expected
} catch (error) {
// Handle validation error - unexpected properties found
console.error("API response validation failed:", error);
}
// Safe parsing with error handling
const result = UserSchema.safeParse(apiResponse);
if (result.success) {
const user = result.data;
// Process validated user data
} else {
// Handle validation errors gracefully
console.warn("Invalid user data:", result.error.issues);
}
Best Practices
- Start with strip mode (default) for most applications
- Use strict mode for critical data validation where API contract compliance is essential
- Use loose mode when working with external APIs that may add fields over time
- Always use
.safeParse()when validation failures are expected - Test with real API data to ensure your chosen mode works with actual responses
- Document your choice in your project's README for team members