Skip to content

Commit 7283d0e

Browse files
ymc9claude
andauthored
fix(orm): handle cyclic JSON typedef references in zod factory (#2654) (#2655)
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent d1db37c commit 7283d0e

2 files changed

Lines changed: 77 additions & 1 deletion

File tree

packages/orm/src/client/zod/factory.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -384,7 +384,11 @@ export class ZodSchemaFactory<
384384
const schema = z.looseObject(
385385
Object.fromEntries(
386386
Object.entries(typeDef.fields).map(([field, def]) => {
387-
let fieldSchema = this.makeScalarSchema(def.type);
387+
// Wrap nested typedef references in z.lazy() so cyclic or self-referencing
388+
// typedefs don't recurse infinitely while building schemas.
389+
let fieldSchema: ZodType = isTypeDef(this.schema, def.type)
390+
? z.lazy(() => this.makeTypeDefSchema(def.type))
391+
: this.makeScalarSchema(def.type);
388392
if (def.array) {
389393
fieldSchema = fieldSchema.array();
390394
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import { createTestClient } from '@zenstackhq/testtools';
2+
import { describe, expect, it } from 'vitest';
3+
4+
// https://github.com/zenstackhq/zenstack/issues/2654
5+
describe('Regression for issue 2654', () => {
6+
it('handles cyclic references between JSON typedefs', async () => {
7+
const schema = `
8+
type A {
9+
name String
10+
b B
11+
}
12+
13+
type B {
14+
a A[]
15+
x String
16+
}
17+
18+
model User {
19+
id String @id @default(cuid())
20+
a A @json
21+
}
22+
`;
23+
24+
const db = await createTestClient(schema, { provider: 'postgresql' });
25+
26+
const data = {
27+
id: 'u1',
28+
a: { name: 'abc', b: { x: '123', a: [] } },
29+
};
30+
await expect(db.user.create({ data })).resolves.toMatchObject(data);
31+
32+
const nested = {
33+
id: 'u2',
34+
a: {
35+
name: 'root',
36+
b: {
37+
x: 'b1',
38+
a: [{ name: 'inner', b: { x: 'b2', a: [] } }],
39+
},
40+
},
41+
};
42+
await expect(db.user.create({ data: nested })).resolves.toMatchObject(nested);
43+
});
44+
45+
it('handles self-referencing JSON typedef', async () => {
46+
const schema = `
47+
type Tree {
48+
name String
49+
children Tree[]?
50+
}
51+
52+
model Node {
53+
id String @id @default(cuid())
54+
tree Tree @json
55+
}
56+
`;
57+
58+
const db = await createTestClient(schema, { provider: 'postgresql' });
59+
60+
const data = {
61+
id: 'n1',
62+
tree: {
63+
name: 'root',
64+
children: [
65+
{ name: 'child1', children: [] },
66+
{ name: 'child2', children: [{ name: 'grandchild', children: [] }] },
67+
],
68+
},
69+
};
70+
await expect(db.node.create({ data })).resolves.toMatchObject(data);
71+
});
72+
});

0 commit comments

Comments
 (0)