Skip to content

Fix null vs nullable, and add a OpenAPI version switch, starting with v3.1#343

Open
philsturgeon wants to merge 3 commits into
elysiajs:mainfrom
philsturgeon:feat/oas3-1
Open

Fix null vs nullable, and add a OpenAPI version switch, starting with v3.1#343
philsturgeon wants to merge 3 commits into
elysiajs:mainfrom
philsturgeon:feat/oas3-1

Conversation

@philsturgeon
Copy link
Copy Markdown

@philsturgeon philsturgeon commented Jun 1, 2026

Hello friends. I've been tasked with updated Speakeasy's Elysia integration guide, and noticed you've got a new OpenAPI plugin to replace the Swagger plugin. Thank you!

You've made the first step, but there's a few issues in this otherwise fantastic approach you're taking to making OpenAPI a first-class part of the web framework.

  1. There's no version selection. People need to pick v3.0, v3.1, v3.2, as different tools have different levels of support. Many are up to v3.2 now, but some are still lagging, so people need to be able to set the version back a bit.
  2. Your current OAS 3.0 support is invalid, it's putting type: null into the YAML and that's one of the major differences between OpenAPI Schema and JSON Schema.

Now that OpenAPI v3.1 is fully JSON Schema compatible, I've been helping tools migrate from OpenAPI v3.0 to v3.1 for years. I wrote this guide to help see what other differences: https://www.openapis.org/blog/2021/02/16/migrating-from-openapi-3-0-to-3-1-0

I don't think you do anything much with exclusiveMinimum and example vs examples is fine as its just deprecated not removed. That means you could honestly claim OpenAPI v3.1 support with this switch, and adding nullable: true to OAS 3.0 is fixing a bug.

This is a double win I recon.

Let me know as soon as OpenAPI v3.1 is supported so I can pop it up on OpenAPI.Tools. We don't add new "legacy" tools (those that dont support at least 3.1), but the Elysia OpenAPI support is so good I'm eager to raise awareness of your framework to the OpenAPI community.

Relates to #343, #299

Summary by CodeRabbit

  • New Features

    • Configurable OpenAPI version specification—now supports OpenAPI 3.0.x and 3.1.x versions with a default of 3.1.2
    • Updated schema normalization to handle version-specific requirements for null handling and JSON Schema dialects
  • Documentation

    • Updated README with the new openapiVersion configuration option and supported version ranges

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 1, 2026

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f0c91d3a-7772-46f9-813f-079d7d7087c3

📥 Commits

Reviewing files that changed from the base of the PR and between 58a9f7a and 129898d.

📒 Files selected for processing (2)
  • src/openapi.ts
  • test/openapi/null-to-openapi.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/openapi.ts

Walkthrough

Adds configurable OpenAPI document version support (3.0.x and 3.1.x): types/config option, version normalization and injection into generated docs, OpenAPI-version–aware schema normalization (including nullable handling), and expanded tests and README docs.

Changes

Configurable OpenAPI Version Support

Layer / File(s) Summary
Type contract and configuration schema
src/types.ts
Adds OpenAPIVersion (`3.0.${number}
Core version handling and plugin wiring
src/index.ts
Introduces DEFAULT_OPENAPI_VERSION, normalizeOpenAPIVersion(), accepts openapiVersion in plugin config, computes effectiveOpenAPIVersion, injects it into generated document openapi field, updates internal cached type to OpenAPIDocument, and re-exports OpenAPIVersion.
OpenAPI schema normalization and null handling
src/openapi.ts
Refactors toRef; extends unwrapSchema() to accept openapiVersion and choose JSON Schema draft; adds normalizeSchemaForOpenAPIVersion and exported nullToOpenApi; threads openapiVersion through toOpenAPISchema for params, bodies, responses, and components.schemas; tightens TypeBox schema detection in response mapping.
Documentation and tests
README.md, test/index.test.ts, test/openapi/null-to-openapi.test.ts
Documents openapiVersion option and supported ranges; renames test suite to OpenAPI; adds tests for emitted OpenAPI 3.0.x/3.1.x, jsonSchemaDialect forwarding, provider compatibility and embedded spec rendering; adds version-specific nullable-union and metadata-preservation tests for null-to-OpenAPI conversion.

Sequence Diagram

sequenceDiagram
  participant Types as src/types.ts
  participant Index as src/index.ts
  participant SchemaGen as src/openapi.ts
  participant Tests as test/*

  Types->>Index: export OpenAPIVersion and updated config types
  Index->>SchemaGen: call toOpenAPISchema(..., openapiVersion)
  SchemaGen->>SchemaGen: unwrapSchema(..., openapiVersion) selects draft & normalizes nullable/null
  SchemaGen->>Index: return OpenAPIDocument with top-level openapi set
  Index->>Tests: tests validate emitted OpenAPI versions and nullable handling
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

にゃ〜ん、見てなさいよ♡ (¬‿¬)
Versions twirl: 3.0 and 3.1〜♡
Nulls cosplay as arrays or nullable〜♪
Types, schema, tests all sing in tune〜♡
CI will nod and the specs will bloom〜 (っ˘ω˘ς)

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly captures the two main changes: fixing null/nullable handling and adding OpenAPI version selection starting with v3.1 support.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

src/openapi.ts

ESLint skipped: missing config or dependency (missing-dependency). The ESLint configuration references a package that is not available in the sandbox.

test/openapi/null-to-openapi.test.ts

ESLint skipped: the ESLint configuration for this file references a package that is not available in the sandbox.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/index.ts (1)

102-102: 💤 Low value

Pfft, casting something that's already that type? Silly~ ♡(╹◡╹)

effectiveOpenAPIVersion is already typed OpenAPIVersion at Line 90, so as OpenAPIVersion here is dead weight. Drop it.

♻️ tiny tidy-up
-			openapi: effectiveOpenAPIVersion as OpenAPIVersion,
+			openapi: effectiveOpenAPIVersion,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/index.ts` at line 102, Remove the redundant type cast in the object
passed from the main logic in src/index.ts: `effectiveOpenAPIVersion` is already
an `OpenAPIVersion`, so drop the unnecessary `as OpenAPIVersion` in the
`openapi` assignment and leave the existing typed value as-is.
test/index.test.ts (1)

30-42: ⚡ Quick win

Testing 3.0 with the easiest possible route? How conveniently cowardly of you~ ♡(˘ω˘)

This 3.0.x test only validates default routes, so it never exercises a nullable/t.Null() schema. That's exactly the case that would expose the version mismatch flagged in src/openapi.ts (Lines 540-545): a 3.0.1 doc carrying type: 'null' should fail SwaggerParser.validate. Adding a nullable-union case under 3.0.x would either confirm the gap or prove me wrong~

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/index.test.ts` around lines 30 - 42, The test for OpenAPI 3.0.x only
covers default routes and never exercises a nullable or t.Null() schema, which
may hide a version mismatch issue. Modify the test inside the 'it emits OpenAPI
3.0.x when configured' block to add a route that returns or includes a nullable
union schema to ensure the emitted OpenAPI 3.0.1 document is validated for
nullable support and that SwaggerParser.validate properly fails or passes as
expected.
src/types.ts (1)

61-65: ⚡ Quick win

Yeah yeah, lazy typing surelyyyy ♡ Just use the union the library gives you, dummy~

The intersection Partial<OpenAPIV3.Document> & Partial<OpenAPIV3_1.Document> technically works here, but that's just dumb when openapi-types already exports a union type for multi-version support: OpenAPI.Document is literally OpenAPIV2.Document | OpenAPIV3.Document | OpenAPIV3_1.Document [1]. Intersecting version-specific types is risky (properties with incompatible types can collapse to never), and the library docs recommend unions, not intersections, for handling multiple OpenAPI versions [2].

Use Omit<OpenAPI.Document, ...> instead and stop reinventing the wheel~ (´∇`)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/types.ts` around lines 61 - 65, Replace the intersection-based type used
for the documentation property with the library-provided union: change the type
on documentation (currently Omit<Partial<OpenAPIV3.Document> &
Partial<OpenAPIV3_1.Document>, 'x-express-openapi-additional-middleware' |
'x-express-openapi-validation-strict'>) to use Omit<OpenAPI.Document,
'x-express-openapi-additional-middleware' |
'x-express-openapi-validation-strict'> so you rely on the union exported by
openapi-types (OpenAPI.Document) instead of intersecting version-specific types.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openapi.ts`:
- Around line 540-545: The current code unconditionally emits JSON Schema using
schema['~standard'].jsonSchema(... { target: 'draft-2020-12' }) and then runs
enumToOpenApi, which produces 3.1-style null/type shapes that break when
openapiVersion is 3.0.x; update the logic where enumToOpenApi is invoked (and
any response-building code that special-cases type === 'null') to branch on the
effective openapiVersion value (threaded from src/index.ts), calling the schema
generator with a 3.0-compatible dialect/target (or draft-07) when openapiVersion
startsWith '3.0' and transforming emitted schemas so nullable types become
nullable: true (remove type: 'null' entries or anyOf containing null and set
nullable: true on the non-null schema) before passing to enumToOpenApi; ensure
the response/content creation code also treats nullable via nullable:true for
3.0.x.

---

Nitpick comments:
In `@src/index.ts`:
- Line 102: Remove the redundant type cast in the object passed from the main
logic in src/index.ts: `effectiveOpenAPIVersion` is already an `OpenAPIVersion`,
so drop the unnecessary `as OpenAPIVersion` in the `openapi` assignment and
leave the existing typed value as-is.

In `@src/types.ts`:
- Around line 61-65: Replace the intersection-based type used for the
documentation property with the library-provided union: change the type on
documentation (currently Omit<Partial<OpenAPIV3.Document> &
Partial<OpenAPIV3_1.Document>, 'x-express-openapi-additional-middleware' |
'x-express-openapi-validation-strict'>) to use Omit<OpenAPI.Document,
'x-express-openapi-additional-middleware' |
'x-express-openapi-validation-strict'> so you rely on the union exported by
openapi-types (OpenAPI.Document) instead of intersecting version-specific types.

In `@test/index.test.ts`:
- Around line 30-42: The test for OpenAPI 3.0.x only covers default routes and
never exercises a nullable or t.Null() schema, which may hide a version mismatch
issue. Modify the test inside the 'it emits OpenAPI 3.0.x when configured' block
to add a route that returns or includes a nullable union schema to ensure the
emitted OpenAPI 3.0.1 document is validated for nullable support and that
SwaggerParser.validate properly fails or passes as expected.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f6f3fea-95e6-4763-9054-e3c7a7cc320e

📥 Commits

Reviewing files that changed from the base of the PR and between dabaa94 and f3bddcb.

⛔ Files ignored due to path filters (1)
  • src/gen/index.ts is excluded by !**/gen/**
📒 Files selected for processing (5)
  • README.md
  • src/index.ts
  • src/openapi.ts
  • src/types.ts
  • test/index.test.ts

Comment thread src/openapi.ts Outdated
@philsturgeon philsturgeon changed the title Support OpenAPI v3.1 and complain about unsupported versions Fix null vs nullable, and add a OpenAPI version switch, starting with v3.1 Jun 2, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/openapi.ts (1)

262-266: 💤 Low value

Forgetting to pass openapiVersion here, sloppy~ ♡ ʕ •ᴥ•ʔ

mergeSchemaProperty and unwrapResponseSchema call unwrapSchema(...) without forwarding openapiVersion, so the merge of standalone-validator schemas is normalized with the hardcoded default 3.1.2. It mostly recovers because toOpenAPISchema re-unwraps the merged result later (and normalizeNullableSchemaForOAS30 can re-collapse a type: ['x','null'] produced by the first pass), but relying on a double round-trip to undo the wrong version is fragile. Thread openapiVersion through these helpers for a single, consistent normalization.

Also applies to: 294-320

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/openapi.ts` around lines 262 - 266, mergeSchemaProperty and
unwrapResponseSchema call unwrapSchema(...) without forwarding openapiVersion,
causing standalone-validator schemas to be normalized using the default 3.1.2;
update both mergeSchemaProperty and unwrapResponseSchema to accept and pass the
openapiVersion parameter through to unwrapSchema (and any intermediate calls) so
unwrapSchema(incomingSchema, vendors, openapiVersion) is used everywhere, and
adjust any call sites and function signatures accordingly (also ensure to
propagate openapiVersion into any code paths mentioned like toOpenAPISchema and
normalizeNullableSchemaForOAS30 where a consistent version is required).
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/openapi.ts`:
- Around line 640-714: normalizeNullableSchemaForOAS30 (and similarly
normalizeNullableSchemaForOAS31) currently recurses into every object key via
Object.entries and treats arbitrary keyword values (e.g., default) as schemas,
causing corruption like stripping type:'null'; change the recursion to only
descend into known schema-bearing keys (e.g., properties, patternProperties,
items, additionalProperties, unevaluatedProperties, allOf, anyOf, oneOf, not,
prefixItems, items, dependencies/schema-valued keywords, etc.) and into array
elements of those specific keys, leaving other keyword objects (like default,
examples, metadata) untouched; update both normalizeNullableSchemaForOAS30 and
normalizeNullableSchemaForOAS31 to check key names before calling
normalizeNullableSchemaForOAS30/31 recursively and only transform { type: 'null'
} when it is actually a schema node.

---

Nitpick comments:
In `@src/openapi.ts`:
- Around line 262-266: mergeSchemaProperty and unwrapResponseSchema call
unwrapSchema(...) without forwarding openapiVersion, causing
standalone-validator schemas to be normalized using the default 3.1.2; update
both mergeSchemaProperty and unwrapResponseSchema to accept and pass the
openapiVersion parameter through to unwrapSchema (and any intermediate calls) so
unwrapSchema(incomingSchema, vendors, openapiVersion) is used everywhere, and
adjust any call sites and function signatures accordingly (also ensure to
propagate openapiVersion into any code paths mentioned like toOpenAPISchema and
normalizeNullableSchemaForOAS30 where a consistent version is required).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e35e0dd6-de29-4812-b718-0663c9217769

📥 Commits

Reviewing files that changed from the base of the PR and between f3bddcb and 58a9f7a.

📒 Files selected for processing (4)
  • src/index.ts
  • src/openapi.ts
  • test/index.test.ts
  • test/openapi/null-to-openapi.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • test/index.test.ts

Comment thread src/openapi.ts
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

Successfully merging this pull request may close these issues.

1 participant