Skip to main content

Depth Control

Manage object nesting and prevent infinite recursion in complex schemas.

The Problem

Recursive schemas can cause infinite loops:

interface Category {
id: string;
name: string;
parent?: Category;
children?: Category[];
}

// Without depth control, this could recurse infinitely
const categoryFactory = new Factory<Category>((faker) => ({
id: faker.string.uuid(),
name: faker.commerce.department(),
parent: categoryFactory.build(), // ⚠️ Infinite recursion
children: categoryFactory.batch(3), // ⚠️ Infinite recursion
}));

The Solution: maxDepth

Set maximum nesting depth to prevent infinite recursion:

const categoryFactory = new Factory<Category>(
factoryFn,
{ maxDepth: 3 }, // Stop at 3 levels deep
);

How It Works

When maxDepth is reached:

  • Factory returns empty object {}
  • Zod schemas return appropriate fallback values
  • Prevents stack overflow errors

Factory Example

interface TreeNode {
id: string;
value: string;
children: TreeNode[];
}

const treeFactory = new Factory<TreeNode>(
(faker) => ({
id: faker.string.uuid(),
value: faker.lorem.word(),
children:
faker.number.int({ min: 0, max: 3 }) > 0
? treeFactory.batch(2) // Recursive call
: [],
}),
{ maxDepth: 4 },
);

const tree = treeFactory.build();
// Tree will be at most 4 levels deep

ZodFactory Example

const categorySchema: z.ZodType<Category> = z.lazy(() =>
z.object({
id: z.string().uuid(),
name: z.string(),
children: z.array(categorySchema).optional(),
}),
);

const factory = new ZodFactory(categorySchema, { maxDepth: 3 });
const category = factory.build();
// Automatically limits nesting to 3 levels

Best Practices

Design Schemas for Depth Limiting

// ❌ Required nested objects fail at depth limit
const problematicSchema = z.object({
level1: z.object({
level2: z.object({
level3: z.object({
value: z.string(), // Required - will fail at maxDepth=2
}),
}),
}),
});

// ✅ Optional nested objects work with depth limiting
const compatibleSchema = z.object({
level1: z
.object({
level2: z
.object({
level3: z
.object({
value: z.string(),
})
.optional(), // Optional - works with maxDepth
})
.optional(),
})
.optional(),
});

Use Conditional Logic

const factory = new Factory<TreeNode>((faker, iteration) => {
const currentDepth = getCurrentDepth(); // Custom depth tracking

return {
id: faker.string.uuid(),
value: faker.lorem.word(),
children:
currentDepth < 3
? factory.batch(faker.number.int({ min: 0, max: 2 }))
: [], // Stop recursion manually
};
});

Lazy References

import { Ref } from 'interface-forge';

const parentRef = new Ref<Category>();

const categoryFactory = new Factory<Category>((faker) => ({
id: faker.string.uuid(),
name: faker.commerce.department(),
parent: parentRef.get(), // Lazy reference
children: [],
}));

// Set reference after creation to avoid recursion
const parent = categoryFactory.build();
parentRef.set(parent);

Custom Depth Handlers

For ZodFactory, implement custom handlers for recursive types:

const factory = new ZodFactory(recursiveSchema, {
maxDepth: 3,
}).withTypeHandler('ZodLazy', (schema, generator, currentDepth) => {
if (currentDepth >= 2) {
return null; // Stop recursion early
}

return {
id: generator.factory.string.uuid(),
children: generator.factory.helpers.maybe(() => [
factory.build(),
factory.build(),
]),
};
});

Performance Considerations

Memory Usage

// ❌ Deep nesting uses lots of memory
const deepFactory = new Factory<DeepObject>(
factoryFn,
{ maxDepth: 20 }, // Very deep
);

// ✅ Reasonable depth for most use cases
const reasonableFactory = new Factory<DeepObject>(
factoryFn,
{ maxDepth: 5 }, // Reasonable depth
);

Generation Speed

// Test different depths for performance
console.time('depth-3');
const shallow = factory.build(); // maxDepth: 3
console.timeEnd('depth-3');

console.time('depth-10');
const deep = factory.build(); // maxDepth: 10
console.timeEnd('depth-10');

Testing with Depth Control

describe('Tree Generation', () => {
it('should respect depth limits', () => {
const factory = new Factory<TreeNode>(factoryFn, { maxDepth: 3 });

const tree = factory.build();

// Verify maximum depth
const maxDepth = calculateDepth(tree);
expect(maxDepth).toBeLessThanOrEqual(3);
});

it('should handle different depths', () => {
[1, 3, 5].forEach((depth) => {
const factory = new Factory<TreeNode>(factoryFn, {
maxDepth: depth,
});

const tree = factory.build();
expect(calculateDepth(tree)).toBeLessThanOrEqual(depth);
});
});
});

function calculateDepth(node: TreeNode): number {
if (!node.children?.length) return 1;
return 1 + Math.max(...node.children.map(calculateDepth));
}

Default Depth

The universal default is DEFAULT_MAX_DEPTH = 5, providing a good balance between complexity and performance for most use cases.