Skip to content

feat(nullable): nullable.go for OpenAPI 3.1 compatibility#2068

Closed
frenchi wants to merge 1 commit into
oapi-codegen:mainfrom
frenchi:feat/3.1/nullable
Closed

feat(nullable): nullable.go for OpenAPI 3.1 compatibility#2068
frenchi wants to merge 1 commit into
oapi-codegen:mainfrom
frenchi:feat/3.1/nullable

Conversation

@frenchi

@frenchi frenchi commented Aug 29, 2025

Copy link
Copy Markdown

Summary

Hello maintainers 👋

New contributor to this project, so feedback very much welcome & appreciated!

We just upgraded our internal OAS Spec to version 3.1, before running in to #373 3.1 compatibility issues. Instead of downgrading, I have some cycles to spend on hopefully progressing some of the tasks under OpenAPI 3.1 support.

This is the first in a small series of PRs to hopefully make progress towards the changed nullable behaviour in OAS 3.1, while ensuring backwards compatibility.

Open Questions up-front:

This PR

nullable.go handles null in line with JSON schema core 2020-12 spec for OAS 3.1, with one explicit generator policy:
Empty schemas {} are treated as non-nullable to avoid over-detection in codegen.

What's included:

  • Centralised 3.1 null detector in pkg/codegen/nullable.go (this PR) but not wired up yet.
    • Exports IsSchemaNullable(specVersion string, schema *openapi3.SchemaRef) bool
    • Keeps default behaviour unchanged (i.e. output-options.nullable-type exists in configuration-schema.json, default false)
    • 3.1: Considers type (including union with 'null'), oneOf/anyOf/allOf, enum (null present), and not (blocks null if it accepts null)
    • 3.0: Falls back to schema.Value.Nullable for nullable: true compatibility
    • a single file keeps logic consistent and avoids scattered special cases throughout the codebase.
  • Tests in pkg/codegen/nullability_test.go

Note: The detector intentionally diverges from raw JSON Schema for {} as explained above.

The logic & test coverage of oneOf/anyOf/allOf got a little hairy, so I found I understood it better once I sketched it out.

  • General rule (where k is “how many subschemas explicitly allow null”):
    • anyOf: k ≥ 1
    • oneOf: k = 1
    • allOf: k = total number of subschemas

e.g.

  • [ {'type':'string'} ] is zero subschemas, k=0
  • [ {'type':'null'} ] is one subschema, k=1
  • [ {'type':'null'}, {'type':'null'} ] k=2, etc...
k anyOf(null) oneOf(null) allOf(null)
0
1
2+ ✅ (only if all explicitly allow null)

appendix below with full test coverage matrix.

Future Work

  • If above PR is accepted, integrate IsSchemaNullable() into the broader codebase,
  • Feature-flag: Wire up existing output-options.nullable-type config
  • Version gating and safety
    • Detect spec version; enable 3.1 paths only when openapi: 3.1.*
    • Provide unit-test coverage for detector behavior on both 3.0 and 3.1 inputs

Delivery plan

rough PR split

  • PR1: detector (unused) + tests <- this PR
  • PR2: Explicit feature-flag & initial schema integration,
  • PR3: Version gating & safety,
  • PR4?: Cleanup, docs touch-ups, internal refactors if needed

Checklist

  • Tests added/updated (for new features or bug fixes)
  • Documentation updated (README/CONTRIBUTING/examples as needed) N/A
  • make tidy is run locally
  • make test is run locally
  • make generate run is run locally and outputs committed (if generated code is affected) N/A
  • make lint is run locally

Additional context

Test Coverage of all cases:

Case Description Combinator Example k assert Covered by test
Type-only null type type: 'null' 1 TestIsSchemaNullable_31_TypeOnlyNull
Type-only string type type: 'string' 0 TestIsSchemaNullable_31_TypeOnlyString
Type union includes null type type: ['string','null'] 1 TestIsSchemaNullable_31_TypeUnionIncludesNull
Type union, no null type type: ['string','integer'] 0 TestIsSchemaNullable_31_NonNullable
oneOf with single null-only arm oneOf [ {type:'string'}, {type:'null'} ] 1 TestIsSchemaNullable_31_OneOfWithNullOnly
oneOf with two null-only arms oneOf [ {type:'null'}, {type:'null'} ] 2 TestIsSchemaNullable_31_OneOf_AllNullArms
oneOf mixed, multiple null-allowing arms oneOf [ {type:['string','null']}, {type:'null'}, {type:'integer'} ] 2 TestIsSchemaNullable_31_OneOf_MixedMultipleNullAllowingArms
anyOf with single null-only arm anyOf [ {type:'integer'}, {type:'null'} ] 1 TestIsSchemaNullable_31_AnyOfWithNullOnly
anyOf with two null-only arms anyOf [ {type:'null'}, {type:'null'} ] 2 TestIsSchemaNullable_31_AnyOf_AllNullArms
allOf with one null-only arm allOf [ {type:'null'}, {type:'string'} ] 1 TestIsSchemaNullable_31_AllOf_OneNullArm
allOf with all null-only arms allOf [ {type:'null'}, {type:'null'} ] 2 TestIsSchemaNullable_31_AllOf_AllNullArms
Base type blocks null in anyOf anyOf + type type:'string', anyOf: [ {type:'null'}, {minLength:1} ] 1 TestIsSchemaNullable_31_BaseTypeBlocksNull_InAnyOf
enum includes null (no type) enum enum: [null, 'x'] 1 TestIsSchemaNullable_31_EnumAllowsNull_NoType
enum includes null but base type excludes enum + base type:'string', enum: [null, 'x'] 1 TestIsSchemaNullable_31_EnumAllowsNull_WithTypeString_Disallowed
not blocks null not type: ['string','null'], not: {type:'null'} 1 TestIsSchemaNullable_31_NotBlocksNull
empty schema (policy) {} TestIsSchemaNullable_31_EmptySchema_NotNullableByPolicy
OAS 3.0 nullable true 3.0 {type:'string', nullable:true} TestIsSchemaNullable_30_FallbackTrue
OAS 3.0 default false 3.0 {type:'string'} TestIsSchemaNullable_30_FallbackFalse

Introduce pkg/codegen/nullable.go with IsSchemaNullable(specVersion, openapi3.SchemaRef)
- Support 3.1 JSON Schema null patterns: type: ["T","null"], oneOf/anyOf with null-only arm
- Fall back to 3.0 schema.Nullable
- add tests pkg/codegen/nullable_test.go
- No integration yet; generation behavior unchanged until wired in
@frenchi
frenchi requested a review from a team as a code owner August 29, 2025 05:10
@kusari-inspector

Copy link
Copy Markdown

Kusari Inspector

Kusari Analysis Results:

Proceed with these changes

✅ No Flagged Issues Detected
All values appear to be within acceptable risk parameters.

No pinned version dependency changes, code issues or exposed secrets detected!

@kusari-inspector rerun - Trigger a re-analysis of this PR
@kusari-inspector feedback [your message] - Send feedback to our AI and team
See Kusari's documentation for setup and configuration.
Commit: 752211e, performed at: 2025-08-29T05:10:44Z

Found this helpful? Give it a 👍 or 👎 reaction!

@mromaszewicz

Copy link
Copy Markdown
Member

Hi @frenchi , thanks for doing this, however, now that kin-openapi supports OpenAPI 3.1, we're doing this on top of that code, and it'll be in the next minor release. I'm closing this since we'll do it differently.

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.

2 participants