Description
Hello! Love this project and been using it for a while, I've come up to the following scenario and would maybe like your input on this and see if it makes sense.
Setting
OpenAPI discriminators are not generating narrowed TypeScript types or Zod discriminated unions which provides a not ideal developer experience
Minimal Reproducible Example
{
"openapi": "3.1.0",
"info": {
"title": "Example API",
"version": "1.0.0"
},
"servers": [
{
"url": "https://api.example.com"
}
],
"paths": {},
"components": {
"schemas": {
"Event": {
"type": "object",
"additionalProperties": true,
"discriminator": {
"propertyName": "type",
"mapping": {
"created": "#/components/schemas/CreatedEvent",
"updated": "#/components/schemas/UpdatedEvent"
}
},
"properties": {
"type": {
"type": "string"
},
"timestamp": {
"type": "string",
"format": "date-time"
}
},
"required": ["type"]
},
"CreatedEvent": {
"additionalProperties": true,
"allOf": [
{
"$ref": "#/components/schemas/Event"
},
{
"type": "object",
"properties": {
"resourceId": {
"type": "string"
}
}
}
],
"required": ["type", "resourceId"]
},
"UpdatedEvent": {
"additionalProperties": true,
"allOf": [
{
"$ref": "#/components/schemas/Event"
},
{
"type": "object",
"properties": {
"resourceId": {
"type": "string"
},
"changes": {
"type": "object"
}
}
}
],
"required": ["type", "resourceId"]
}
}
}
}
Current generation of types & schemas
types.gen.ts:
export type Event = {
type: string; // here's where the discriminator could use it
timestamp?: string;
[key: string]: unknown | string | undefined;
};
export type CreatedEvent = Omit<Event, 'type'> & {
resourceId: string;
type: 'created';
};
export type UpdatedEvent = Omit<Event, 'type'> & {
resourceId: string;
changes?: Record<string, unknown>;
type: 'updated';
};
zod.gen.ts
export const zEvent = z.object({
type: z.string(), // just string
union
timestamp: z.optional(z.iso.datetime())
});
export const zCreatedEvent = zEvent.and(z.object({
resourceId: z.string(),
type: z.literal('created')
}));
export const zUpdatedEvent = zEvent.and(z.object({
resourceId: z.string(),
changes: z.optional(z.record(z.unknown())),
type: z.literal('updated')
}));
Issues:
- Base Event type has type: string instead of narrowed
- union 'created' | 'updated'
- No union type is generated combining all variants
- Zod uses .and() merging instead of z.discriminatedUnion()
- Results in slower runtime validation and no automatic type narrowing
Desired Behavior
Types:
export type Event =
| CreatedEvent
| UpdatedEvent;
export type CreatedEvent = {
type: 'created';
timestamp?: string;
resourceId: string;
};
export type UpdatedEvent = {
type: 'updated';
timestamp?: string;
resourceId: string;
changes?: Record<string, unknown>;
};
Zod:
export const zCreatedEvent = z.object({
type: z.literal('created'),
timestamp: z.optional(z.iso.datetime())
resourceId: z.string()
});
export const zUpdatedEvent = z.object({
type: z.literal('updated'),
timestamp: z.optional(z.iso.datetime())
resourceId: z.string(),
changes: z.optional(z.record(z.unknown()))
});
// Discriminated union
export const zEvent = z.discriminatedUnion('type', [
zCreatedEvent,
zUpdatedEvent
]);
Environment
- @hey-api/openapi-ts: v0.90.10
- OpenAPI version: 3.1.0
- Plugins: @hey-api/typescript, zod
openapi-ts.ts
import { defineConfig } from '@hey-api/openapi-ts';
export default defineConfig({
input: './openapi-spec.json',
output: {
path: './src/generated',
},
plugins: [
{
name: '@hey-api/typescript',
style: 'PascalCase',
},
{
name: 'zod',
exportFromIndex: true,
},
],
});
Description
Hello! Love this project and been using it for a while, I've come up to the following scenario and would maybe like your input on this and see if it makes sense.
Setting
OpenAPI discriminators are not generating narrowed TypeScript types or Zod discriminated unions which provides a not ideal developer experience
Minimal Reproducible Example
{ "openapi": "3.1.0", "info": { "title": "Example API", "version": "1.0.0" }, "servers": [ { "url": "https://api.example.com" } ], "paths": {}, "components": { "schemas": { "Event": { "type": "object", "additionalProperties": true, "discriminator": { "propertyName": "type", "mapping": { "created": "#/components/schemas/CreatedEvent", "updated": "#/components/schemas/UpdatedEvent" } }, "properties": { "type": { "type": "string" }, "timestamp": { "type": "string", "format": "date-time" } }, "required": ["type"] }, "CreatedEvent": { "additionalProperties": true, "allOf": [ { "$ref": "#/components/schemas/Event" }, { "type": "object", "properties": { "resourceId": { "type": "string" } } } ], "required": ["type", "resourceId"] }, "UpdatedEvent": { "additionalProperties": true, "allOf": [ { "$ref": "#/components/schemas/Event" }, { "type": "object", "properties": { "resourceId": { "type": "string" }, "changes": { "type": "object" } } } ], "required": ["type", "resourceId"] } } } }Current generation of types & schemas
types.gen.ts:
zod.gen.ts
Issues:
Desired Behavior
Types:
Zod:
Environment
openapi-ts.ts