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’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Invalid OpenAPI spec being generated #178

Open
lifenautjoe opened this issue Dec 14, 2024 · 3 comments
Open

Invalid OpenAPI spec being generated #178

lifenautjoe opened this issue Dec 14, 2024 · 3 comments

Comments

@lifenautjoe
Copy link

Hi, it seems that the resulting OpenAPI spec being generated when using references as suggested here, generates an invalid OpenAPI spec definition.

This is my model:

import type { Static } from "elysia";
import { t } from "elysia";
import { APILinkRedditPostSchema } from "./redditPost.schema";
import { APILinkGithubRepositorySchema } from "./githubRepository.schema";
import { APILinkRedditSubredditSchema } from "./redditSubreddit.schema";
import { APILinkRedditUserSchema } from "./redditUser.schema";
import { APILinkTiktokVideoSchema } from "./tiktokVideo.schema";
import { APILinkTiktokUserSchema } from "./tiktokUser.schema";
import { APILinkEtsyProductSchema } from "./etsyProduct.schema";
import { APILinkAmazonProductSchema } from "./amazonProduct.schema";
import { APILinkYoutubeVideoSchema } from "./youtubeVideo.schema";
import { APILinkInstagramPostSchema } from "./instagramPost.schema";
import { APILinkInstagramUserSchema } from "./instagramUser.schema";
import { APILinkXPostSchema } from "./xPost.schema";
import { APILinkYoutubeUserSchema } from "./youtubeUser.schema";
import { APILinkXUserSchema } from "./xUser.schema";
import { APILinkIconSchema } from "./icon.schema";
import { APILinkResponsiveImageSchema } from "./responsiveImage.schema";
import { APILinkVideoSchema } from "./video.schema";
import { APILinkAudioSchema } from "./audio.schema";
import { APILinkDocumentSchema } from "./document.schema";
import { APILinkTextSchema } from "./text.schema";
import { APILinkPageSchema } from "./page.schema";
import { APILinkTypeSchema } from "./type.schema";

export const APILinkSchema = t.Object(
  {
    id: t.Number({
      description: "Unique identifier for the link",
      examples: [123456789, 987654321],
    }),
    ok: t.Boolean({
      description: "Whether the link is valid",
      examples: [true, false],
    }),
    requestId: t.Optional(
      t.String({
        description: "Optional identifier for tracking the request",
        examples: ["req_123abc", "req_xyz789"],
      })
    ),
    updatedAt: t.String({
      format: "date-time",
      description: "Last update timestamp",
      examples: ["2023-10-15T12:00:00Z", "2023-09-01T15:30:00Z"],
    }),
    url: t.String({
      description: "Original URL",
      examples: ["https://example.com/page", "https://website.com/article"],
    }),
    domain: t.String({
      description: "Domain name from the URL",
      examples: ["example.com", "website.com"],
    }),
    type: t.Ref<typeof APILinkTypeSchema>("#/components/schemas/APILinkType"),
    status: t.Number({
      description: "HTTP status code",
      examples: [200, 404, 500],
    }),
    size: t.Optional(
      t.Number({
        description: "Content size in bytes",
        examples: [1024, 2048, 4096],
      })
    ),
    redirected: t.Boolean({
      description: "Whether the URL was redirected",
      examples: [true, false],
    }),
    redirectionUrl: t.Optional(
      t.String({
        description: "Final URL after redirects",
        examples: [
          "https://example.com/final",
          "https://website.com/destination",
        ],
      })
    ),
    redirectionCount: t.Optional(
      t.Number({
        description: "Number of redirects",
        examples: [1, 2, 3],
      })
    ),
    redirectionTrail: t.Optional(
      t.Array(
        t.String({
          description: "Redirect URL",
          examples: [
            "https://example.com/redirect1",
            "https://example.com/redirect2",
          ],
        })
      )
    ),
    title: t.Optional(
      t.String({
        description: "Page title (only missing when ok: false)",
        examples: ["Welcome to Example", "Article Title"],
      })
    ),
    description: t.Optional(
      t.String({
        description: "Page description (only missing when ok: false)",
        examples: [
          "A sample page description",
          "Learn more about our services",
        ],
      })
    ),
    language: t.Optional(
      t.String({
        description: "Content language code",
        examples: ["en", "es", "fr"],
      })
    ),
    icon: t.Optional(
      t.Ref<typeof APILinkIconSchema>("#/components/schemas/Icon")
    ),
    image: t.Optional(
      t.Ref<typeof APILinkResponsiveImageSchema>(
        "#/components/schemas/ResponsiveImage"
      )
    ),
    video: t.Optional(
      t.Ref<typeof APILinkVideoSchema>("#/components/schemas/Video")
    ),
    audio: t.Optional(
      t.Ref<typeof APILinkAudioSchema>("#/components/schemas/Audio")
    ),
    document: t.Optional(
      t.Ref<typeof APILinkDocumentSchema>("#/components/schemas/Document")
    ),
    text: t.Optional(
      t.Ref<typeof APILinkTextSchema>("#/components/schemas/Text")
    ),
    page: t.Optional(
      t.Ref<typeof APILinkPageSchema>("#/components/schemas/Page")
    ),
    youtubeVideo: t.Optional(
      t.Ref<typeof APILinkYoutubeVideoSchema>(
        "#/components/schemas/YouTubeVideo"
      )
    ),
    youtubeUser: t.Optional(
      t.Ref<typeof APILinkYoutubeUserSchema>("#/components/schemas/YoutubeUser")
    ),
    xUser: t.Optional(
      t.Ref<typeof APILinkXUserSchema>("#/components/schemas/XUser")
    ),
    xPost: t.Optional(
      t.Ref<typeof APILinkXPostSchema>("#/components/schemas/XPost")
    ),
    instagramUser: t.Optional(
      t.Ref<typeof APILinkInstagramUserSchema>(
        "#/components/schemas/InstagramUser"
      )
    ),
    instagramPost: t.Optional(
      t.Ref<typeof APILinkInstagramPostSchema>(
        "#/components/schemas/InstagramPost"
      )
    ),
    amazonProduct: t.Optional(
      t.Ref<typeof APILinkAmazonProductSchema>(
        "#/components/schemas/AmazonProduct"
      )
    ),
    etsyProduct: t.Optional(
      t.Ref<typeof APILinkEtsyProductSchema>("#/components/schemas/EtsyProduct")
    ),
    tiktokUser: t.Optional(
      t.Ref<typeof APILinkTiktokUserSchema>("#/components/schemas/TiktokUser")
    ),
    tiktokVideo: t.Optional(
      t.Ref<typeof APILinkTiktokVideoSchema>("#/components/schemas/TiktokVideo")
    ),
    redditUser: t.Optional(
      t.Ref<typeof APILinkRedditUserSchema>("#/components/schemas/RedditUser")
    ),
    redditSubreddit: t.Optional(
      t.Ref<typeof APILinkRedditSubredditSchema>(
        "#/components/schemas/RedditSubreddit"
      )
    ),
    redditPost: t.Optional(
      t.Ref<typeof APILinkRedditPostSchema>("#/components/schemas/RedditPost")
    ),
    githubRepository: t.Optional(
      t.Ref<typeof APILinkGithubRepositorySchema>(
        "#/components/schemas/GithubRepository"
      )
    ),
  },
  {
    $id: "#/components/schemas/Link",
    description: "Complete information about a link and its associated content",
  }
);

export type APILink = Static<typeof APILinkSchema>;

I then add it to the app as a .model

const app = new Elysia()
  .use(
    swagger({
      documentation: {
        info: {
          title: "Peekalink",
          version: "1.0.0",
        },
        components: {
          securitySchemes: {
            bearerAuth: {
              type: "http",
              scheme: "bearer",
            },
          },
        },
        tags: [
          {
            name: "Link Preview",
            description: "Endpoints related to link preview generation",
          },
          {
            name: "Service Status",
            description: "Endpoints related to service status and monitoring",
          },
        ],
      },
      path: "/swagger", // The path where Swagger UI will be served
    })
  )
  .model({
    Link: APILinkSchema,
    Icon: APILinkIconSchema,
    ResponsiveImage: APILinkResponsiveImageSchema,
    Video: APILinkVideoSchema,
    Audio: APILinkAudioSchema,
    Document: APILinkDocumentSchema,
    Text: APILinkTextSchema,
    Page: APILinkPageSchema,
    YouTubeVideo: APILinkYoutubeVideoSchema,
    YoutubeUser: APILinkYoutubeUserSchema,
    XUser: APILinkXUserSchema,
    XPost: APILinkXPostSchema,
    InstagramUser: APILinkInstagramUserSchema,
    InstagramPost: APILinkInstagramPostSchema,
    AmazonProduct: APILinkAmazonProductSchema,
    EtsyProduct: APILinkEtsyProductSchema,
    TiktokUser: APILinkTiktokUserSchema,
    TiktokVideo: APILinkTiktokVideoSchema,
    RedditUser: APILinkRedditUserSchema,
    RedditSubreddit: APILinkRedditSubredditSchema,
    RedditPost: APILinkRedditPostSchema,
    GithubRepository: APILinkGithubRepositorySchema,
    LinkPreviewRequestBody: LinkPreviewRequestBodySchema,
    APIErrorResponseBody: APIErrorResponseBodySchema,
    APIHealthResponseBody: APIHealthResponseBodySchema,
    APILinkSource: APILinkSourceSchema,
    APILinkType: APILinkTypeSchema,
  })
  .use(cors())
  .use(handleRoot)
  .use(handleUser)
  .use(handleScreenshot)
  .use(handleApiAnalytics)
  .use(handleApiRequests)
  .use(handleHealth)
  .onError(async ({ code, error }) => {
    // This is the fallback, since only root and is-available have error handlers
    return getErrorResponse({
      error,
      code,
    });
  })
  .listen(process.env.PORT || 3000);

baseLogger.info({ port: app.server?.port }, "Server started");

export type ElysiaApp = typeof app;

This is the resulting OpenAPI spec.

The relevant Link part is the following:

{
"$id": "#/components/schemas/Link",
"description": "Complete information about a link and its associated content",
"type": "object",
"properties": {
"id": {
"description": "Unique identifier for the link",
"examples": [
123456789,
987654321
],
"type": "number"
},
"ok": {
"description": "Whether the link is valid",
"examples": [
true,
false
],
"type": "boolean"
},
"requestId": {
"description": "Optional identifier for tracking the request",
"examples": [
"req_123abc",
"req_xyz789"
],
"type": "string"
},
"updatedAt": {
"format": "date-time",
"description": "Last update timestamp",
"examples": [
"2023-10-15T12:00:00Z",
"2023-09-01T15:30:00Z"
],
"type": "string"
},
"url": {
"description": "Original URL",
"examples": [
"https://example.com/page",
"https://website.com/article"
],
"type": "string"
},
"domain": {
"description": "Domain name from the URL",
"examples": [
"example.com",
"website.com"
],
"type": "string"
},
"type": {
"$ref": "#/components/schemas/APILinkType"
},
"status": {
"description": "HTTP status code",
"examples": [
200,
404,
500
],
"type": "number"
},
"size": {
"description": "Content size in bytes",
"examples": [
1024,
2048,
4096
],
"type": "number"
},
"redirected": {
"description": "Whether the URL was redirected",
"examples": [
true,
false
],
"type": "boolean"
},
"redirectionUrl": {
"description": "Final URL after redirects",
"examples": [
"https://example.com/final",
"https://website.com/destination"
],
"type": "string"
},
"redirectionCount": {
"description": "Number of redirects",
"examples": [
1,
2,
3
],
"type": "number"
},
"redirectionTrail": {
"type": "array",
"items": {
"description": "Redirect URL",
"examples": [
"https://example.com/redirect1",
"https://example.com/redirect2"
],
"type": "string"
}
},
"title": {
"description": "Page title (only missing when ok: false)",
"examples": [
"Welcome to Example",
"Article Title"
],
"type": "string"
},
"description": {
"description": "Page description (only missing when ok: false)",
"examples": [
"A sample page description",
"Learn more about our services"
],
"type": "string"
},
"language": {
"description": "Content language code",
"examples": [
"en",
"es",
"fr"
],
"type": "string"
},
"icon": {
"$ref": "#/components/schemas/Icon"
},
"image": {
"$ref": "#/components/schemas/ResponsiveImage"
},
"video": {
"$ref": "#/components/schemas/Video"
},
"audio": {
"$ref": "#/components/schemas/Audio"
},
"document": {
"$ref": "#/components/schemas/Document"
},
"text": {
"$ref": "#/components/schemas/Text"
},
"page": {
"$ref": "#/components/schemas/Page"
},
"youtubeVideo": {
"$ref": "#/components/schemas/YouTubeVideo"
},
"youtubeUser": {
"$ref": "#/components/schemas/YoutubeUser"
},
"xUser": {
"$ref": "#/components/schemas/XUser"
},
"xPost": {
"$ref": "#/components/schemas/XPost"
},
"instagramUser": {
"$ref": "#/components/schemas/InstagramUser"
},
"instagramPost": {
"$ref": "#/components/schemas/InstagramPost"
},
"amazonProduct": {
"$ref": "#/components/schemas/AmazonProduct"
},
"etsyProduct": {
"$ref": "#/components/schemas/EtsyProduct"
},
"tiktokUser": {
"$ref": "#/components/schemas/TiktokUser"
},
"tiktokVideo": {
"$ref": "#/components/schemas/TiktokVideo"
},
"redditUser": {
"$ref": "#/components/schemas/RedditUser"
},
"redditSubreddit": {
"$ref": "#/components/schemas/RedditSubreddit"
},
"redditPost": {
"$ref": "#/components/schemas/RedditPost"
},
"githubRepository": {
"$ref": "#/components/schemas/GithubRepository"
}
},
"required": [
"id",
"ok",
"updatedAt",
"url",
"domain",
"type",
"status",
"redirected"
]
}

Perhaps is because we added a manual $id that is leaking into the final OpenAPI spec?

Is there an official way of referencing OpenAPI models in different schemas?

Thank you!

@lifenautjoe
Copy link
Author

Upon further investigation, it seems that the examples which is being outputted on the OpenAPI definition is the one making the spec invalid.

That and the $id attributes.

@lifenautjoe
Copy link
Author

Found several issues I had to monkeypatch in order for the generated OpenAPI JSON spec to be valid.

  1. examples must be removeed
  2. No t.Literals usage anywhere, used t.Enum instead
  3. No t.Any usage anywhere
  4. No $id attribute anywhere
  5. $ref's are generated under response status codes like 200: {$ref: "..."} , those $refs should be removed.

It seems that the generator has some issues generating 100% compliant docs.

After all this was done, I could use mintlify to generate automatic docs from the endpoint.

If you're struggling with this, use the https://editor.swagger.io/ to load your JSON file and see what errors you're getting. The errors you get is what you have to remove/tweak.

@lifenautjoe
Copy link
Author

One last thing, the resulting OpenAPI json file also don't contain the security attributes defined on individual routes.

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

1 participant