astahmer / openapi-zod-client

Generate a zodios (typescript http client with zod validation) from an OpenAPI spec (json/yaml)
openapi-zod-client.vercel.app
823 stars 89 forks source link

Unhashed Schema Names? #19

Closed robotkutya closed 2 years ago

robotkutya commented 2 years ago

Is there a way to generate all OpenApi component schemas and export them (without hashed names)?

In the Pet Store example, there are 8 component schemas. https://github.com/astahmer/openapi-zod-client/blob/main/example/petstore.yaml

Only 4 of them are added to export const schemas {...}. https://github.com/astahmer/openapi-zod-client/blob/main/example/petstore-schemas.ts

astahmer commented 2 years ago

hey, this is intended behavior as the main purpose of this lib is to get an api client (using zodios) which means we will only export schemas used by the openapi PathsObject (doc.paths)

however, using the exported API from openapi-zod-client I think you can achieve what you want:

// ./example/petstore-components-generator.ts
import SwaggerParser from "@apidevtools/swagger-parser";
import { OpenAPIObject, SchemaObject } from "openapi3-ts";
import { ConversionTypeContext, getZodSchema } from "../src/openApiToZod";

const main = async () => {
    const openApiDoc = (await SwaggerParser.parse("./example/petstore.yaml")) as OpenAPIObject;
    const schemas = openApiDoc.components?.schemas ?? {};

    const ctx: ConversionTypeContext = {
        getSchemaByRef: (ref) => schemas[ref.split("/").at(-1)!] as SchemaObject,
        zodSchemaByHash: {},
        schemaHashByRef: {},
        codeMetaByRef: {},
        hashByVariableName: {},
        circularTokenByRef: {},
    };
    const zodSchemas = Object.fromEntries(
        Object.entries(schemas).map(([name, schema]) => [name, getZodSchema({ schema, ctx }).toString()])
    );
    console.log(zodSchemas);
};

main();
❯ pnpm tsx ./example/petstore-components-generator.ts
{
  Order: 'z.object({ id: z.number(), petId: z.number(), quantity: z.number(), shipDate: z.string(), status: z.enum(["placed", "approved", "delivered"]), complete: z.boolean() }).partial()',
  Customer: 'z.object({ id: z.number(), username: z.string(), address: z.array(@ref__vjRfEADnJZ8__) }).partial()',
  Address: 'z.object({ street: z.string(), city: z.string(), state: z.string(), zip: z.string() }).partial()',
  Category: 'z.object({ id: z.number(), name: z.string() }).partial()',
  User: 'z.object({ id: z.number(), username: z.string(), firstName: z.string(), lastName: z.string(), email: z.string(), password: z.string(), phone: z.string(), userStatus: z.number() }).partial()',
  Tag: 'z.object({ id: z.number(), name: z.string() }).partial()',
  Pet: 'z.object({ id: z.number().optional(), name: z.string(), category: @ref__vR1x0k5qaLk__.optional(), photoUrls: z.array(z.string()), tags: z.array(@ref__vR1x0k5qaLk__).optional(), status: z.enum(["available", "pending", "sold"]).optional() })',
  ApiResponse: 'z.object({ code: z.number(), type: z.string(), message: z.string() }).partial()'
}

is that ok ?

astahmer commented 2 years ago

btw I forgot to talk about the @ref_xxx tokens still here

tl;dr, a naive solution: add something like this inlineRefTokenWithActualSchema

const inlineRefTokenWithActualSchema = (code: string) =>
        code.replaceAll(tokens.refTokenHashRegex, (match) => ctx.zodSchemaByHash[match]);

    const zodSchemas = Object.fromEntries(
        Object.entries(schemas).map(([name, schema]) => [
            name,
            inlineRefTokenWithActualSchema(getZodSchema({ schema, ctx }).toString()),
        ])
    );

output:

❯ pnpm tsx ./example/petstore-components-generator.ts
{
  Order: 'z.object({ id: z.number(), petId: z.number(), quantity: z.number(), shipDate: z.string(), status: z.enum(["placed", "approved", "delivered"]), complete: z.boolean() }).partial()',
  Customer: 'z.object({ id: z.number(), username: z.string(), address: z.array(z.object({ street: z.string(), city: z.string(), state: z.string(), zip: z.string() }).partial()) }).partial()',
  Address: 'z.object({ street: z.string(), city: z.string(), state: z.string(), zip: z.string() }).partial()',
  Category: 'z.object({ id: z.number(), name: z.string() }).partial()',
  User: 'z.object({ id: z.number(), username: z.string(), firstName: z.string(), lastName: z.string(), email: z.string(), password: z.string(), phone: z.string(), userStatus: z.number() }).partial()',
  Tag: 'z.object({ id: z.number(), name: z.string() }).partial()',
  Pet: 'z.object({ id: z.number().optional(), name: z.string(), category: z.object({ id: z.number(), name: z.string() }).partial().optional(), photoUrls: z.array(z.string()), tags: z.array(z.object({ id: z.number(), name: z.string() }).partial()).optional(), status: z.enum(["available", "pending", "sold"]).optional() })',
  ApiResponse: 'z.object({ code: z.number(), type: z.string(), message: z.string() }).partial()'
}

the thing is that there might be more complex cases than the petstore one, with recursive refs etc, or multiple schemas referencing another, which leads to duplicated code

which is why I went the @ref_xxx tokens way, even tho the code has become quite a mess (sorry for that I was in a hurry, what started as a quick PoC somehow got a bit of attention so I tried to deliver ASAP)

robotkutya commented 2 years ago

Thanks, that looks very promising! I'll take a look at the code after some sleep :P

Yeah, I figured that this was intended behavior. Maybe consider adding this as a feature behind a flag? Something similar to the --exportModels and --exportSchemas flags in openapi-typescript-codege[]

astahmer commented 2 years ago

yeah ok this definitely makes sense, I just published a new version (v0.2.2) with that feature (--export-schemas option), tell me if everything works fine for you ! 🙏

robotkutya commented 2 years ago

Daaaaamn, that was fast! Beautiful!

Seems to work perfectly.

Thank you so much 🖖