UNPKG

@alexop/openapi-zod-client

Version:

[![Screenshot 2022-11-12 at 18 52 25](https://user-images.githubusercontent.com/47224540/201487856-ffc4c862-6f31-4de1-8ef1-3981fabf3416.png)](https://openapi-zod-client.vercel.app/)

1,486 lines (1,470 loc) 146 kB
import SwaggerParser from "@apidevtools/swagger-parser"; import type { OpenAPIObject, SchemasObject } from "openapi3-ts"; import { beforeAll, describe, expect, test } from "vitest"; import { generateZodClientFromOpenAPI } from "./generateZodClientFromOpenAPI"; import { getZodClientTemplateContext } from "./template-context"; import { pathToVariableName } from "./utils"; let openApiDoc: OpenAPIObject; beforeAll(async () => { openApiDoc = (await SwaggerParser.parse("./tests/petstore.yaml")) as OpenAPIObject; }); test("getZodClientTemplateContext", async () => { const result = getZodClientTemplateContext(openApiDoc); expect(result).toMatchInlineSnapshot(` { "circularTypeByName": {}, "emittedType": {}, "endpoints": [ { "description": "Update an existing pet by Id", "errors": [ { "description": "Invalid ID supplied", "schema": "z.void()", "status": 400, }, { "description": "Pet not found", "schema": "z.void()", "status": 404, }, { "description": "Validation exception", "schema": "z.void()", "status": 405, }, ], "method": "put", "parameters": [ { "description": "Update an existent pet in the store", "name": "body", "schema": "Pet", "type": "Body", }, ], "path": "/pet", "requestFormat": "json", "response": "Pet", }, { "description": "Add a new pet to the store", "errors": [ { "description": "Invalid input", "schema": "z.void()", "status": 405, }, ], "method": "post", "parameters": [ { "description": "Create a new pet in the store", "name": "body", "schema": "Pet", "type": "Body", }, ], "path": "/pet", "requestFormat": "json", "response": "Pet", }, { "description": "Returns a single pet", "errors": [ { "description": "Invalid ID supplied", "schema": "z.void()", "status": 400, }, { "description": "Pet not found", "schema": "z.void()", "status": 404, }, ], "method": "get", "parameters": [ { "name": "petId", "schema": "z.number().int()", "type": "Path", }, ], "path": "/pet/:petId", "requestFormat": "json", "response": "Pet", }, { "description": "", "errors": [ { "description": "Invalid input", "schema": "z.void()", "status": 405, }, ], "method": "post", "parameters": [ { "name": "petId", "schema": "z.number().int()", "type": "Path", }, { "name": "name", "schema": "z.string().optional()", "type": "Query", }, { "name": "status", "schema": "z.string().optional()", "type": "Query", }, ], "path": "/pet/:petId", "requestFormat": "json", "response": "z.void()", }, { "description": "delete a pet", "errors": [ { "description": "Invalid pet value", "schema": "z.void()", "status": 400, }, ], "method": "delete", "parameters": [ { "name": "api_key", "schema": "z.string().optional()", "type": "Header", }, { "name": "petId", "schema": "z.number().int()", "type": "Path", }, ], "path": "/pet/:petId", "requestFormat": "json", "response": "z.void()", }, { "description": "", "errors": [], "method": "post", "parameters": [ { "description": undefined, "name": "body", "schema": "z.instanceof(File)", "type": "Body", }, { "name": "petId", "schema": "z.number().int()", "type": "Path", }, { "name": "additionalMetadata", "schema": "z.string().optional()", "type": "Query", }, ], "path": "/pet/:petId/uploadImage", "requestFormat": "binary", "response": "ApiResponse", }, { "description": "Multiple status values can be provided with comma separated strings", "errors": [ { "description": "Invalid status value", "schema": "z.void()", "status": 400, }, ], "method": "get", "parameters": [ { "name": "status", "schema": "z.enum(["available", "pending", "sold"]).optional().default("available")", "type": "Query", }, ], "path": "/pet/findByStatus", "requestFormat": "json", "response": "z.array(Pet)", }, { "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", "errors": [ { "description": "Invalid tag value", "schema": "z.void()", "status": 400, }, ], "method": "get", "parameters": [ { "name": "tags", "schema": "z.array(z.string()).optional()", "type": "Query", }, ], "path": "/pet/findByTags", "requestFormat": "json", "response": "z.array(Pet)", }, { "description": "Returns a map of status codes to quantities", "errors": [], "method": "get", "parameters": [], "path": "/store/inventory", "requestFormat": "json", "response": "z.record(z.number().int())", }, { "description": "Place a new order in the store", "errors": [ { "description": "Invalid input", "schema": "z.void()", "status": 405, }, ], "method": "post", "parameters": [ { "description": undefined, "name": "body", "schema": "Order", "type": "Body", }, ], "path": "/store/order", "requestFormat": "json", "response": "Order", }, { "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generate exceptions.", "errors": [ { "description": "Invalid ID supplied", "schema": "z.void()", "status": 400, }, { "description": "Order not found", "schema": "z.void()", "status": 404, }, ], "method": "get", "parameters": [ { "name": "orderId", "schema": "z.number().int()", "type": "Path", }, ], "path": "/store/order/:orderId", "requestFormat": "json", "response": "Order", }, { "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", "errors": [ { "description": "Invalid ID supplied", "schema": "z.void()", "status": 400, }, { "description": "Order not found", "schema": "z.void()", "status": 404, }, ], "method": "delete", "parameters": [ { "name": "orderId", "schema": "z.number().int()", "type": "Path", }, ], "path": "/store/order/:orderId", "requestFormat": "json", "response": "z.void()", }, { "description": "This can only be done by the logged in user.", "errors": [], "method": "post", "parameters": [ { "description": "Created user object", "name": "body", "schema": "User", "type": "Body", }, ], "path": "/user", "requestFormat": "json", "response": "z.void()", }, { "description": "", "errors": [ { "description": "Invalid username supplied", "schema": "z.void()", "status": 400, }, { "description": "User not found", "schema": "z.void()", "status": 404, }, ], "method": "get", "parameters": [ { "name": "username", "schema": "z.string()", "type": "Path", }, ], "path": "/user/:username", "requestFormat": "json", "response": "User", }, { "description": "This can only be done by the logged in user.", "errors": [], "method": "put", "parameters": [ { "description": "Update an existent user in the store", "name": "body", "schema": "User", "type": "Body", }, { "name": "username", "schema": "z.string()", "type": "Path", }, ], "path": "/user/:username", "requestFormat": "json", "response": "z.void()", }, { "description": "This can only be done by the logged in user.", "errors": [ { "description": "Invalid username supplied", "schema": "z.void()", "status": 400, }, { "description": "User not found", "schema": "z.void()", "status": 404, }, ], "method": "delete", "parameters": [ { "name": "username", "schema": "z.string()", "type": "Path", }, ], "path": "/user/:username", "requestFormat": "json", "response": "z.void()", }, { "description": "Creates list of users with given input array", "errors": [], "method": "post", "parameters": [ { "description": undefined, "name": "body", "schema": "z.array(User)", "type": "Body", }, ], "path": "/user/createWithList", "requestFormat": "json", "response": "User", }, { "description": "", "errors": [ { "description": "Invalid username/password supplied", "schema": "z.void()", "status": 400, }, ], "method": "get", "parameters": [ { "name": "username", "schema": "z.string().optional()", "type": "Query", }, { "name": "password", "schema": "z.string().optional()", "type": "Query", }, ], "path": "/user/login", "requestFormat": "json", "response": "z.string()", }, { "description": "", "errors": [], "method": "get", "parameters": [], "path": "/user/logout", "requestFormat": "json", "response": "z.void()", }, ], "endpointsGroups": {}, "options": { "baseUrl": "", "withAlias": false, }, "schemas": { "ApiResponse": "z.object({ code: z.number().int(), type: z.string(), message: z.string() }).partial().passthrough()", "Category": "z.object({ id: z.number().int(), name: z.string() }).partial().passthrough()", "Order": "z.object({ id: z.number().int(), petId: z.number().int(), quantity: z.number().int(), shipDate: z.string().datetime({ offset: true }), status: z.enum(["placed", "approved", "delivered"]), complete: z.boolean() }).partial().passthrough()", "Pet": "z.object({ id: z.number().int().optional(), name: z.string(), category: Category.optional(), photoUrls: z.array(z.string()), tags: z.array(Tag).optional(), status: z.enum(["available", "pending", "sold"]).optional() }).passthrough()", "Tag": "z.object({ id: z.number().int(), name: z.string() }).partial().passthrough()", "User": "z.object({ id: z.number().int(), username: z.string(), firstName: z.string(), lastName: z.string(), email: z.string(), password: z.string(), phone: z.string(), userStatus: z.number().int() }).partial().passthrough()", }, "types": {}, } `); }); describe("generateZodClientFromOpenAPI", () => { test("without options", async () => { const prettyOutput = await generateZodClientFromOpenAPI({ openApiDoc, disableWriteToFile: true }); expect(prettyOutput).toMatchInlineSnapshot(` "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; import { z } from "zod"; const Category = z .object({ id: z.number().int(), name: z.string() }) .partial() .passthrough(); const Tag = z .object({ id: z.number().int(), name: z.string() }) .partial() .passthrough(); const Pet = z .object({ id: z.number().int().optional(), name: z.string(), category: Category.optional(), photoUrls: z.array(z.string()), tags: z.array(Tag).optional(), status: z.enum(["available", "pending", "sold"]).optional(), }) .passthrough(); const ApiResponse = z .object({ code: z.number().int(), type: z.string(), message: z.string() }) .partial() .passthrough(); const Order = z .object({ id: z.number().int(), petId: z.number().int(), quantity: z.number().int(), shipDate: z.string().datetime({ offset: true }), status: z.enum(["placed", "approved", "delivered"]), complete: z.boolean(), }) .partial() .passthrough(); const User = z .object({ id: z.number().int(), username: z.string(), firstName: z.string(), lastName: z.string(), email: z.string(), password: z.string(), phone: z.string(), userStatus: z.number().int(), }) .partial() .passthrough(); export const schemas = { Category, Tag, Pet, ApiResponse, Order, User, }; const endpoints = makeApi([ { method: "put", path: "/pet", description: \`Update an existing pet by Id\`, requestFormat: "json", parameters: [ { name: "body", description: \`Update an existent pet in the store\`, type: "Body", schema: Pet, }, ], response: Pet, errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Pet not found\`, schema: z.void(), }, { status: 405, description: \`Validation exception\`, schema: z.void(), }, ], }, { method: "post", path: "/pet", description: \`Add a new pet to the store\`, requestFormat: "json", parameters: [ { name: "body", description: \`Create a new pet in the store\`, type: "Body", schema: Pet, }, ], response: Pet, errors: [ { status: 405, description: \`Invalid input\`, schema: z.void(), }, ], }, { method: "get", path: "/pet/:petId", description: \`Returns a single pet\`, requestFormat: "json", parameters: [ { name: "petId", type: "Path", schema: z.number().int(), }, ], response: Pet, errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Pet not found\`, schema: z.void(), }, ], }, { method: "post", path: "/pet/:petId", requestFormat: "json", parameters: [ { name: "petId", type: "Path", schema: z.number().int(), }, { name: "name", type: "Query", schema: z.string().optional(), }, { name: "status", type: "Query", schema: z.string().optional(), }, ], response: z.void(), errors: [ { status: 405, description: \`Invalid input\`, schema: z.void(), }, ], }, { method: "delete", path: "/pet/:petId", description: \`delete a pet\`, requestFormat: "json", parameters: [ { name: "api_key", type: "Header", schema: z.string().optional(), }, { name: "petId", type: "Path", schema: z.number().int(), }, ], response: z.void(), errors: [ { status: 400, description: \`Invalid pet value\`, schema: z.void(), }, ], }, { method: "post", path: "/pet/:petId/uploadImage", requestFormat: "binary", parameters: [ { name: "body", type: "Body", schema: z.instanceof(File), }, { name: "petId", type: "Path", schema: z.number().int(), }, { name: "additionalMetadata", type: "Query", schema: z.string().optional(), }, ], response: ApiResponse, }, { method: "get", path: "/pet/findByStatus", description: \`Multiple status values can be provided with comma separated strings\`, requestFormat: "json", parameters: [ { name: "status", type: "Query", schema: z .enum(["available", "pending", "sold"]) .optional() .default("available"), }, ], response: z.array(Pet), errors: [ { status: 400, description: \`Invalid status value\`, schema: z.void(), }, ], }, { method: "get", path: "/pet/findByTags", description: \`Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.\`, requestFormat: "json", parameters: [ { name: "tags", type: "Query", schema: z.array(z.string()).optional(), }, ], response: z.array(Pet), errors: [ { status: 400, description: \`Invalid tag value\`, schema: z.void(), }, ], }, { method: "get", path: "/store/inventory", description: \`Returns a map of status codes to quantities\`, requestFormat: "json", response: z.record(z.number().int()), }, { method: "post", path: "/store/order", description: \`Place a new order in the store\`, requestFormat: "json", parameters: [ { name: "body", type: "Body", schema: Order, }, ], response: Order, errors: [ { status: 405, description: \`Invalid input\`, schema: z.void(), }, ], }, { method: "get", path: "/store/order/:orderId", description: \`For valid response try integer IDs with value &lt;&#x3D; 5 or &gt; 10. Other values will generate exceptions.\`, requestFormat: "json", parameters: [ { name: "orderId", type: "Path", schema: z.number().int(), }, ], response: Order, errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Order not found\`, schema: z.void(), }, ], }, { method: "delete", path: "/store/order/:orderId", description: \`For valid response try integer IDs with value &lt; 1000. Anything above 1000 or nonintegers will generate API errors\`, requestFormat: "json", parameters: [ { name: "orderId", type: "Path", schema: z.number().int(), }, ], response: z.void(), errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Order not found\`, schema: z.void(), }, ], }, { method: "post", path: "/user", description: \`This can only be done by the logged in user.\`, requestFormat: "json", parameters: [ { name: "body", description: \`Created user object\`, type: "Body", schema: User, }, ], response: z.void(), }, { method: "get", path: "/user/:username", requestFormat: "json", parameters: [ { name: "username", type: "Path", schema: z.string(), }, ], response: User, errors: [ { status: 400, description: \`Invalid username supplied\`, schema: z.void(), }, { status: 404, description: \`User not found\`, schema: z.void(), }, ], }, { method: "put", path: "/user/:username", description: \`This can only be done by the logged in user.\`, requestFormat: "json", parameters: [ { name: "body", description: \`Update an existent user in the store\`, type: "Body", schema: User, }, { name: "username", type: "Path", schema: z.string(), }, ], response: z.void(), }, { method: "delete", path: "/user/:username", description: \`This can only be done by the logged in user.\`, requestFormat: "json", parameters: [ { name: "username", type: "Path", schema: z.string(), }, ], response: z.void(), errors: [ { status: 400, description: \`Invalid username supplied\`, schema: z.void(), }, { status: 404, description: \`User not found\`, schema: z.void(), }, ], }, { method: "post", path: "/user/createWithList", description: \`Creates list of users with given input array\`, requestFormat: "json", parameters: [ { name: "body", type: "Body", schema: z.array(User), }, ], response: User, }, { method: "get", path: "/user/login", requestFormat: "json", parameters: [ { name: "username", type: "Query", schema: z.string().optional(), }, { name: "password", type: "Query", schema: z.string().optional(), }, ], response: z.string(), errors: [ { status: 400, description: \`Invalid username/password supplied\`, schema: z.void(), }, ], }, { method: "get", path: "/user/logout", requestFormat: "json", response: z.void(), }, ]); export const api = new Zodios(endpoints); export function createApiClient(baseUrl: string, options?: ZodiosOptions) { return new Zodios(baseUrl, endpoints, options); } " `); }); test("withAlias as true", async () => { const prettyOutput = await generateZodClientFromOpenAPI({ openApiDoc, disableWriteToFile: true, options: { withAlias: true }, }); expect(prettyOutput).toMatchInlineSnapshot(` "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; import { z } from "zod"; const Category = z .object({ id: z.number().int(), name: z.string() }) .partial() .passthrough(); const Tag = z .object({ id: z.number().int(), name: z.string() }) .partial() .passthrough(); const Pet = z .object({ id: z.number().int().optional(), name: z.string(), category: Category.optional(), photoUrls: z.array(z.string()), tags: z.array(Tag).optional(), status: z.enum(["available", "pending", "sold"]).optional(), }) .passthrough(); const ApiResponse = z .object({ code: z.number().int(), type: z.string(), message: z.string() }) .partial() .passthrough(); const Order = z .object({ id: z.number().int(), petId: z.number().int(), quantity: z.number().int(), shipDate: z.string().datetime({ offset: true }), status: z.enum(["placed", "approved", "delivered"]), complete: z.boolean(), }) .partial() .passthrough(); const User = z .object({ id: z.number().int(), username: z.string(), firstName: z.string(), lastName: z.string(), email: z.string(), password: z.string(), phone: z.string(), userStatus: z.number().int(), }) .partial() .passthrough(); export const schemas = { Category, Tag, Pet, ApiResponse, Order, User, }; const endpoints = makeApi([ { method: "put", path: "/pet", alias: "updatePet", description: \`Update an existing pet by Id\`, requestFormat: "json", parameters: [ { name: "body", description: \`Update an existent pet in the store\`, type: "Body", schema: Pet, }, ], response: Pet, errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Pet not found\`, schema: z.void(), }, { status: 405, description: \`Validation exception\`, schema: z.void(), }, ], }, { method: "post", path: "/pet", alias: "addPet", description: \`Add a new pet to the store\`, requestFormat: "json", parameters: [ { name: "body", description: \`Create a new pet in the store\`, type: "Body", schema: Pet, }, ], response: Pet, errors: [ { status: 405, description: \`Invalid input\`, schema: z.void(), }, ], }, { method: "get", path: "/pet/:petId", alias: "getPetById", description: \`Returns a single pet\`, requestFormat: "json", parameters: [ { name: "petId", type: "Path", schema: z.number().int(), }, ], response: Pet, errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Pet not found\`, schema: z.void(), }, ], }, { method: "post", path: "/pet/:petId", alias: "updatePetWithForm", requestFormat: "json", parameters: [ { name: "petId", type: "Path", schema: z.number().int(), }, { name: "name", type: "Query", schema: z.string().optional(), }, { name: "status", type: "Query", schema: z.string().optional(), }, ], response: z.void(), errors: [ { status: 405, description: \`Invalid input\`, schema: z.void(), }, ], }, { method: "delete", path: "/pet/:petId", alias: "deletePet", description: \`delete a pet\`, requestFormat: "json", parameters: [ { name: "api_key", type: "Header", schema: z.string().optional(), }, { name: "petId", type: "Path", schema: z.number().int(), }, ], response: z.void(), errors: [ { status: 400, description: \`Invalid pet value\`, schema: z.void(), }, ], }, { method: "post", path: "/pet/:petId/uploadImage", alias: "uploadFile", requestFormat: "binary", parameters: [ { name: "body", type: "Body", schema: z.instanceof(File), }, { name: "petId", type: "Path", schema: z.number().int(), }, { name: "additionalMetadata", type: "Query", schema: z.string().optional(), }, ], response: ApiResponse, }, { method: "get", path: "/pet/findByStatus", alias: "findPetsByStatus", description: \`Multiple status values can be provided with comma separated strings\`, requestFormat: "json", parameters: [ { name: "status", type: "Query", schema: z .enum(["available", "pending", "sold"]) .optional() .default("available"), }, ], response: z.array(Pet), errors: [ { status: 400, description: \`Invalid status value\`, schema: z.void(), }, ], }, { method: "get", path: "/pet/findByTags", alias: "findPetsByTags", description: \`Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.\`, requestFormat: "json", parameters: [ { name: "tags", type: "Query", schema: z.array(z.string()).optional(), }, ], response: z.array(Pet), errors: [ { status: 400, description: \`Invalid tag value\`, schema: z.void(), }, ], }, { method: "get", path: "/store/inventory", alias: "getInventory", description: \`Returns a map of status codes to quantities\`, requestFormat: "json", response: z.record(z.number().int()), }, { method: "post", path: "/store/order", alias: "placeOrder", description: \`Place a new order in the store\`, requestFormat: "json", parameters: [ { name: "body", type: "Body", schema: Order, }, ], response: Order, errors: [ { status: 405, description: \`Invalid input\`, schema: z.void(), }, ], }, { method: "get", path: "/store/order/:orderId", alias: "getOrderById", description: \`For valid response try integer IDs with value &lt;&#x3D; 5 or &gt; 10. Other values will generate exceptions.\`, requestFormat: "json", parameters: [ { name: "orderId", type: "Path", schema: z.number().int(), }, ], response: Order, errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Order not found\`, schema: z.void(), }, ], }, { method: "delete", path: "/store/order/:orderId", alias: "deleteOrder", description: \`For valid response try integer IDs with value &lt; 1000. Anything above 1000 or nonintegers will generate API errors\`, requestFormat: "json", parameters: [ { name: "orderId", type: "Path", schema: z.number().int(), }, ], response: z.void(), errors: [ { status: 400, description: \`Invalid ID supplied\`, schema: z.void(), }, { status: 404, description: \`Order not found\`, schema: z.void(), }, ], }, { method: "post", path: "/user", alias: "createUser", description: \`This can only be done by the logged in user.\`, requestFormat: "json", parameters: [ { name: "body", description: \`Created user object\`, type: "Body", schema: User, }, ], response: z.void(), }, { method: "get", path: "/user/:username", alias: "getUserByName", requestFormat: "json", parameters: [ { name: "username", type: "Path", schema: z.string(), }, ], response: User, errors: [ { status: 400, description: \`Invalid username supplied\`, schema: z.void(), }, { status: 404, description: \`User not found\`, schema: z.void(), }, ], }, { method: "put", path: "/user/:username", alias: "updateUser", description: \`This can only be done by the logged in user.\`, requestFormat: "json", parameters: [ { name: "body", description: \`Update an existent user in the store\`, type: "Body", schema: User, }, { name: "username", type: "Path", schema: z.string(), }, ], response: z.void(), }, { method: "delete", path: "/user/:username", alias: "deleteUser", description: \`This can only be done by the logged in user.\`, requestFormat: "json", parameters: [ { name: "username", type: "Path", schema: z.string(), }, ], response: z.void(), errors: [ { status: 400, description: \`Invalid username supplied\`, schema: z.void(), }, { status: 404, description: \`User not found\`, schema: z.void(), }, ], }, { method: "post", path: "/user/createWithList", alias: "createUsersWithListInput", description: \`Creates list of users with given input array\`, requestFormat: "json", parameters: [ { name: "body", type: "Body", schema: z.array(User), }, ], response: User, }, { method: "get", path: "/user/login", alias: "loginUser", requestFormat: "json", parameters: [ { name: "username", type: "Query", schema: z.string().optional(), }, { name: "password", type: "Query", schema: z.string().optional(), }, ], response: z.string(), errors: [ { status: 400, description: \`Invalid username/password supplied\`, schema: z.void(), }, ], }, { method: "get", path: "/user/logout", alias: "logoutUser", requestFormat: "json", response: z.void(), }, ]); export const api = new Zodios(endpoints); export function createApiClient(baseUrl: string, options?: ZodiosOptions) { return new Zodios(baseUrl, endpoints, options); } " `); }); test("withAlias as false", async () => { const prettyOutput = await generateZodClientFromOpenAPI({ openApiDoc, disableWriteToFile: true, options: { withAlias: false }, }); expect(prettyOutput).toMatchInlineSnapshot(` "import { makeApi, Zodios, type ZodiosOptions } from "@zodios/core"; import { z } from "zod"; const Category = z .object({ id: z.number().int(), name: z.string() }) .partial() .passthrough(); const Tag = z .object({ id: z.number().int(), name: z.string() }) .partial() .passt