Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Register custom component that contains Zod schema #126

Open
MattIPv4 opened this issue Apr 27, 2023 · 3 comments
Open

Register custom component that contains Zod schema #126

MattIPv4 opened this issue Apr 27, 2023 · 3 comments

Comments

@MattIPv4
Copy link

馃憢 I'd like to be able to register a custom component that contains a Zod schema:

For example:

const clientErrorResp = {
  description: 'There was an issue with the request sent by the client.',
  content: {
    'application/json': {
      schema: z.object({
        code: z.string().openapi({ description: 'Class of error that was generated.' }),
        message: z.string().openapi({ description: 'Details about the specific error instance.' }),
        errors: z.array(z.object({}).catchall(z.any())).optional().openapi({ description: 'Further information about the error(s) encountered.' }),
      }).strict(),
    },
  },
};

registry.registerComponent('responses', 'ClientError', clientErrorResp);

Unfortunately, it doesn't look like the Zod schema within gets transformed. Is there a method I can use to pre-transform it myself, or is there a way to convince the library to do that automatically?

@MattIPv4
Copy link
Author

MattIPv4 commented Apr 27, 2023

My current hack to get around this is to just include a $ref property in the object, use it as part of a path without registering it as a component, and then manually extract all the refs after:

// Register all the endpoints
const registry = new OpenAPIRegistry();
registry.registerPath({
    // ... whatever
    responses: {
        400: {
            $ref: '#/components/responses/ClientError',
            description: 'There was an issue with the request sent by the client.',
            content: {
                'application/json': {
                    schema: z.object({
                        code: z.string().openapi({ description: 'Class of error that was generated.' }),
                        message: z.string().openapi({ description: 'Details about the specific error instance.' }),
                        errors: z.array(z.object({}).catchall(z.any())).optional().openapi({ description: 'Further information about the error(s) encountered.' }),
                    }).strict().openapi({ $ref: '#/components/schemas/Error' }),
                },
            },
        },
    },
});

// Generate the document
const generated = new OpenAPIGenerator(registry.definitions, '3.0.0').generateDocument({});

// Track all refs found
const refs = new Map();
const replaceRefs = value => {
    if (value && typeof value === 'object' && value.constructor === Object) {
        // If this is a ref with other properties, process it
        const keys = Object.keys(value);
        if (keys.length > 1 && keys.includes('$ref')) {
            const ref = value.$ref;
            const match = ref.match(/^#\/components\/([^\/]+)\/(.+)$/);
            if (match) {
                const existing = refs.get(ref);
                if (existing) {
                    if (!deepEqual(existing.value, value, { strict: true }))
                        throw new Error(`Cannot redefine ref ${ref}`);
                    return { $ref: ref };
                }

                const unref = Object.fromEntries(Object.entries(value).filter(([ key ]) => key !== '$ref'));
                refs.set(ref, { value: value, processed: replaceRefs(unref), type: match[1], name: match[2] });
                return { $ref: ref };
            }
        }

        // This is a regular object, look for refs within it
        return Object.entries(value).reduce((obj, [ key, value ]) => ({
            ...obj,
            [key]: replaceRefs(value),
        }), {});
    } else if (Array.isArray(value)) {
        return value.map(replaceRefs);
    }

    return value;
};

// Replace all refs in paths and components
const paths = replaceRefs(generated.paths || {});
const components = replaceRefs(generated.components || {})

// Return the schema with refs injected
return {
    ...generated,
    paths,
    components: [ ...refs.values() ].reduce((obj, { processed, type, name }) => ({
        ...obj,
        [type]: {
            ...(obj[type] || {}),
            [name]: processed,
        },
    }), components),
};

This is definitely a hack, but at least lets me use refs anywhere in the document on the fly.

@AGalabov
Copy link
Collaborator

AGalabov commented May 9, 2023

@MattIPv4 thank you for raising this up. I do think we can do that for sure. We are currently working on releasing a major release v5.0.0 containing an update on how the API is used for OpenApi 3.1 vs OpenApi 3.0. So for the time being we would focus on delivering this.

However once we are done with it I'd go back and take a look on what we can do for this feature request. In the meantime if you want to, feel free to try and open up a PR which we can review.

@seep
Copy link

seep commented Jan 3, 2024

I found this issue after attempting to share error responses as described in the Reusing Responses section of https://swagger.io/docs/specification/describing-responses/ and would also be interested in a way to accomplish this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

3 participants