Skip to content

docs(downgrader): OpenAPI Generator rust-axum breaks on a nullable byte string with a pattern - #243

Merged
dinwwwh merged 22 commits into
mainfrom
claude/project-thread-fvev8j
Oct 31, 2026
Merged

dinwwwh merged 22 commits into
mainfrom
claude/project-thread-fvev8j

Conversation

@dinwwwh

@dinwwwh dinwwwh commented Oct 31, 2026

Copy link
Copy Markdown
Member

Requested by dinwwwh · project thread

Summary

Zod's z.base64().nullable() writes anyOf: [{ type: "string", contentEncoding: "base64", pattern: "..." }, { type: "null" }]. The downgrader turns it into { type: "string", format: "byte", pattern: "...", nullable: true }, as designed.

OpenAPI Generator 7.26's rust-axum server checks a format: byte pattern with a generated validate_byte_* function that takes &ByteArray. A nullable property's field is a Nullable<ByteArray>, so the model does not compile:

error[E0308]: mismatched types
expected `&ByteArray`, found `&Nullable<ByteArray>`

This adds one sentence to the existing note on OpenAPI Generator's Rust client and Spring server.

What was checked

Each shape was compiled with cargo build from both the 3.1 input and the downgraded 3.0 output:

Shape 3.1 3.0
required nullable base64 with pattern ok fails
optional nullable base64 with pattern ok fails
array of nullable base64 with pattern ok ok
required/optional non-null base64 with pattern ok ok
required/optional nullable base64 without pattern ok ok
nullable string with a pattern but no byte format ok ok

Only README text changes. uncheck passes.


Generated by Claude Code

claude added 22 commits October 31, 2026 09:23
…list $ref limitation

- progenitor reads an anyOf with two string-taking branches, the "null"
  branch counted as one, as a struct that takes only objects.
- NSwag's undefined type for a $ref to a oneOf depends on its first branch.
- $refs into the items or properties of a multi-type `type` list dangle.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…ray/record gap

- A $ref into the allOf of a schema that also holds a $ref resolves one
  member off, since that $ref becomes the first allOf member.
- datamodel-code-generator, even with --strict-nullable, leaves null out
  of an array item or map value that is a nullable array or record.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
openapi-zod-client 1.18 drops `nullable: true` from an anyOf branch, as a
nullable tuple member or union member becomes; its bundled Swagger
Parser rejects openapi 3.1.2, while 3.1.1 reads fine and keeps null.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…y enum and byte getter note

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…lable tuple member

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…ull, unions)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…m and byte branch

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…of z.null()

OpenAPI Generator's PHP client writes no allowed values for the enum of
an array item or map value that is { type: "null" } alone, which the
downgrader writes as { type: "string", nullable: true, enum: [null] },
so it throws on any such array or map that is not empty. Its 3.1 path
lets null through.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…4 arrays

Kiota's TypeScript client reads every value of a oneOf of objects with
the first branch. In 3.0 a const tag becomes a one-value enum, which the
first branch rejects, so a value from any other branch of a Zod
discriminated union loses its tag. It also reads an array of format:
byte strings, which z.array(z.base64()) becomes, as one byte array and
drops it; the C# client throws on such an array. From 3.1 Kiota keeps
both as plain strings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…e records

It keeps null on a nullable record only as an object's property whose
values reject null. As a component, an array item, a map value, a branch
of an anyOf or oneOf, or with values that may be null, the record's type
leaves out null. It reads the 3.1 anyOf with a null branch in all of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…le enums

OpenAPI Generator 7.26 merges the oneOf a nullable enum becomes into one
enum, and its go-server then checks that property or array item with an
AssertstringRequired function it never writes. The existing normalizer
flag avoids it. The 3.1 anyOf with a null branch compiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…on byte and null-only shapes

The Spring server, like the Rust client, does not compile a required
nullable format: byte property (Arrays.equals on a JsonNullable<byte[]>).
It also writes the empty enum the Java clients write for type: null
alone, and the Python FastAPI server writes `class X(, Enum)` for such
a component. All of them compile from the 3.1 input.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
…te string with a pattern

Zod's z.base64().nullable() becomes a nullable format: byte string that
keeps its pattern. OpenAPI Generator 7.26's rust-axum server then checks
the pattern with a function that takes a ByteArray, while the field is a
Nullable<ByteArray>, so the property does not compile, required or
optional. 3.1 input compiles.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VVaw7SHxsMExLa1nyEQJVY
@dinwwwh dinwwwh self-assigned this Oct 31, 2026
@codecov

codecov Bot commented Oct 31, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@codspeed

codspeed Bot commented Oct 31, 2026

Copy link
Copy Markdown

Merging this PR will not alter performance

1 warning

⚠️ 1 benchmark is dominated by system calls

System calls cannot be simulated consistently and are left out of the measure. The reported value understates the real cost. Switch to the Walltime instrument to measure them.

Learn more about system calls

See affected benchmarks

✅ 29 untouched benchmarks
⏩ 2 skipped benchmarks1


Comparing claude/project-thread-fvev8j (95b5cee) with main (e1af7bb)

Open in CodSpeed

Footnotes

  1. 2 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩

@pullfrog pullfrog Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

✅ No new issues found.

Reviewed changes

  • OpenAPI Generator rust-axum nullable byte + pattern note — adds one clause to the existing OAG bullet in packages/downgrader/README.md: in 7.26 the Rust server generator (rust-axum) does not compile a nullable format: byte property, required or optional, that also carries a pattern, as Zod's z.base64() writes.

I checked the load-bearing claim against OAG v7.26.0: in rust-axum/models.mustache an isByteArray property with a pattern emits custom(function = "validate_byte_<class>_<prop>"), whose signature is fn(&ByteArray), while the field is typed Nullable<ByteArray> (required) or Option<Nullable<ByteArray>> (optional) — and ModelUtils.isByteArraySchema matches format: byte, so the branch is reached. The custom validation is emitted outside the {{^isNullable}} guard (unlike the XSS checks), which is exactly the reported E0308. Since the PR's own table shows non-null optional byte + pattern compiles, validator unwraps Option and passes the inner type, so the Nullable<ByteArray> case is what mismatches. The Zod-writes-a-pattern premise is also already documented in the repo's ogen note. uncheck passes for the file.

Pullfrog  | View workflow run | Using deepseek-v4.1-flash (free via Pullfrog for OSS) | 𝕏

@dinwwwh
dinwwwh merged commit e964b31 into main Oct 31, 2026
11 checks passed
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