Read OpenAPI 3.2 documents with the released swagger-parser via a compatibility spec loader

KH
2h ago
+4230-136

Implements the approach proposed in #25477: openapi-generator can read OpenAPI 3.2 documents with the released swagger-parser (2.1.48), without waiting for 3.2 support in swagger-core/swagger-parser.

Today, openapi: 3.2.x gives no OpenAPI object at all. swagger-parser falls back to the Swagger 2 converter and reports attribute openapi is unexpected / attribute swagger is missing, so generation fails.

How it works

  • 3.0/3.1 are not touched. SpecLoader.forInputSpec parses the document exactly as before, with one readLocation. Only when that fails with attribute openapi is unexpected does it read the raw document again, with the same authorization values and timeouts as the parser. If the document declares 3.2.x, it is parsed by Oas32CompatSpecLoader.

  • Oas32CompatSpecLoader rewrites the raw tree before parsing:

    • openapi becomes 3.1.0.
    • 3.2-only members move into x-oas32-* extensions: query, additionalOperations, in: querystring, $self, tag parent/kind, itemSchema, and so on.
    • style: cookie becomes form, with the original kept.
    • Missing response descriptions are filled with "".
    • Relative $refs are made absolute so the parser can be called without a location. The 3.1 dereferencer re-reads the location even for same-file references, which would bypass the rewrite.
  • Moved operations go through the parser too, so they get default values (style/explode) and $ref resolution. They are exposed through Oas32Extensions.allOperations / allOperationsList, each matching the call site's original enumeration order (readOperationsMap / readOperations). These are used in:

    • DefaultGenerator
    • DefaultCodegen (preprocessOpenAPI, fromCallback, fromParameter)
    • AbstractJavaCodegen
    • InlineModelResolver
    • OpenAPINormalizer (including FILTER)
    • ModelUtils

    Documents that did not go through the compat loader enumerate exactly as before. Which documents did is tracked by object identity, not by an extension, so it can't leak into output or be forged by a document.

Verification

  • 3.0/3.1 unchanged. 3_0/petstore, 3_1/petstore, 3_0/callbacks, 3_1/webhooks and a HEAD+POST shared-schema case were generated as java and openapi-yaml with this branch and with master. There is no difference other than the generation timestamp. bin/generate-samples.sh for java-okhttp-gson, python and go-petstore produces no diff.

  • 3.2 documents generate with the default validateSpec:

    • QUERY/PURGE operations, including ones inside callbacks and webhooks, with inline request/response bodies turned into models;
    • same-file and external $refs.

    The generated Java client compiles (mvn compile). java --library feign gets the right Accept/Content-Type headers for them. Generating openapi-yaml twice gives identical output.

  • Oas32CompatSpecLoaderTest plus the existing DefaultGenerator/DefaultCodegen/InlineModelResolver/OpenAPINormalizer/ModelUtils tests pass, including forbiddenapis. The new tests fail without the change.

Known limits (warned at load time where applicable)

  • 3.2-only members inside external documents are not rewritten. That would need the parser's resolver to go through the loader.
  • Schema $id and the root $self are not used as a reference base. The result matches what the 3.1 parser does today.
  • $refs that point into a moved member (e.g. #/paths/~1x/query/...) can't be resolved.
  • Generator-specific preprocessing in languages/ other than AbstractJavaCodegen, plus validate, MergedSpecBuilder and the online service, still call the parser or readOperations* directly.
  • Whether each language's templates can send a non-standard method such as QUERY is the scope of #24982.
  • After the Swagger 2 converter attempt, swagger-parser logs two failed to read resource listing ERROR stack traces even when the 3.2 document then loads fine. An INFO line says so.

Questions for maintainers (from #25477)

  1. Is rewriting a 3.2 document to 3.1 at the generate entry point acceptable as a stopgap until swagger-parser ships 3.2?
  2. Are extension-held operations, exposed through Oas32Extensions, acceptable until swagger-core has the model fields?

This is a large change, mostly in one new class (Oas32CompatSpecLoader) and its tests. I'm happy to split it if you prefer: for example, the SpecLoader refactor first, then the 3.2 loader.

PR checklist

  • Read the contribution guidelines.
  • Run the following to build the project and update samples:
    ./mvnw clean package || exit
    ./bin/generate-samples.sh ./bin/configs/*.yaml || exit
    ./bin/utils/export_docs_generators.sh || exit
    
    Regenerated java-okhttp-gson, python and go-petstore samples: no diff. Generator options and docs are unchanged.
  • If your PR is targeting a particular programming language, @mention the technical committee members. (This is a core change, not language-specific.)

Parts of this change were drafted with AI coding assistants (Devin, Claude Code, with reviews by Codex and Fable) and reviewed and verified by me.

šŸ¤– Generated with Claude Code


Summary by cubic

Lets openapi-generator read OpenAPI 3.2 documents with the released swagger-parser (2.1.48), which otherwise fails with attribute openapi is unexpected. 3.0/3.1 documents keep the existing single parse path unchanged.

  • When the parser fails, the raw document is re-read with the same authorization values and timeouts and, if it declares 3.2.x, rewritten to 3.1.0 before parsing.
  • 3.2-only members (query, additionalOperations, in: querystring, $self, tag parent/kind, itemSchema) move into x-oas32-* extensions; style: cookie becomes form, missing response descriptions are filled, and relative $refs are made absolute.
  • Restored operations go through the parser for defaults and $ref resolution and are exposed via Oas32Extensions to DefaultGenerator, DefaultCodegen, InlineModelResolver, OpenAPINormalizer, ModelUtils, and AbstractJavaCodegen; other documents enumerate exactly as before.
  • A root $self now supplies the base URI for relative $refs and an authored x-oas32-* extension occupying a compat slot is preserved under x-oas32-authored-<key> instead of overwritten. additionalOperations keys colliding with a fixed method remain visible to the normalizer's FILTER but are skipped during operation generation, so no duplicate operation is emitted.
  • Whether a document went through the loader is tracked by object identity, so it can't leak into output or be forged.

Known limits

  • 3.2-only members inside external documents aren't rewritten, $id isn't used as a reference base, and $refs pointing into moved members can't be resolved; validate, MergedSpecBuilder, and the online service still parse directly.
  • Generators serialize in: querystring parameters as regular named query parameters (warned once per load).