trpc-openapi icon indicating copy to clipboard operation
trpc-openapi copied to clipboard

Support for union or discriminatedUnion?

Open jackwlee01 opened this issue 1 year ago • 12 comments

The following code produces a runtime error:

  thing: publicProcedure
    .meta({ openapi: { method: 'GET', path: '/api/thing' }})
    .input( z.union([
      z.object({ type: z.literal('first'), myVar: z.string() }),
      z.object({ type: z.literal('another'), anotherVar: z.number() }),
      z.object({ type: z.literal('third') }),
    ]))
    .output( z.string() )
    .query(({ ctx }) => "thing" ),

TRPCError: [query.thing] - Input parser must be a ZodObject

Is there any planned support for union or discriminatedUnion?

jackwlee01 avatar Feb 01 '23 07:02 jackwlee01

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.

stale[bot] avatar Apr 04 '23 14:04 stale[bot]

I looked into this. Looks like a non-trivial change since zod-to-json-schema simply returns an anyOf set whenever it sees a union with the openapi3 option.

To properly support OpenAPI3 style discriminators, one would have to either modify zod-to-json-schema to support an openapi3 specific output (which would be a significant change for that package) or perform some pre/post processing in zodSchemaToOpenApiSchemaObject.

I think the latter would be better for this library, specifically. However, I still need to investigate further what a preprocessing or post processing step would look like in practice. Open to ideas!

levi avatar Jun 02 '23 05:06 levi

This would be a great feature. Unions are extremely useful for creating strict contracts and type narrowing. I definitely miss having this ability when working with openapi.

ThePaulMcBride avatar Jun 02 '23 14:06 ThePaulMcBride

what happened this support? how can I tackle a discriminatedUnion without using it?

realjesset avatar Jun 09 '23 09:06 realjesset

I wrote a hacky implementation to start a discussion, but realized that my needs were for nested discriminated unions, which are not supported in the OpenAPI 3 spec. I closed my PR, but someone feel free to take a look at the kernel of the PR and open a new one.

levi avatar Jun 15 '23 18:06 levi

I don't understand the challenge since zod-to-json-schema correctly parses unions? anyOf isn't what we want?

baptisteArno avatar Sep 06 '23 09:09 baptisteArno

This is also triggered by z.array()

ntindle avatar Sep 20 '23 04:09 ntindle

+1

popuguytheparrot avatar Oct 05 '23 09:10 popuguytheparrot

I cannot use this library just because it lacks this feature. This is a necessary thing, but it seems like the library has not been functioning actively recently, could it be that it has died?

hoangtrung99 avatar Nov 05 '23 09:11 hoangtrung99

I wrote some logic to handle .input({ appId: z.union([z.number(), z.string()]) }) as originally (1) this would error about the union not being coercible (when technically everything within it is coercible), and additionally I (2) wrote some logic to ensure that the values within the input that is produced are coerced into the correct type. I think this approach is fine with the zod-to-json-schema anyOf set mentioned above...?

Is there something that I'm misunderstanding about this requirement or is my situation a little different/simpler? I can make the PR if what I've done locally is somewhat beneficial.

sebinsua avatar Dec 11 '23 20:12 sebinsua

+1

ferdy-roz avatar Jan 02 '24 19:01 ferdy-roz

This issue could be hacked around like this

const unionSchema = z.discriminatedUnion('type', [
    z.object({ type: z.literal('number'), number: z.number() }),
    z.object({ type: z.literal('string'), string: z.string() }),
]);

const restSchema = z.object({
    type: z.string(),
    number: z.number().optional(),
    string: z.string().optional(),
});

export const restRouter = router({
    test: procedure
        .meta({
            openapi: {
                method: 'GET',
                path: '/test',
            },
        })
        .input(restSchema)
        .output(z.object({}))
        .query(async ({ input }) => {
            const validatedInput = await unionSchema.parseAsync(input);
            return {};
        }),
})

The code is still safe, just with a bit of duplication

tsumo avatar Mar 29 '24 08:03 tsumo