Skip to content

Feature/cu 869byc3qm fix sdk openapi types being optional but shown as any of variant sebastiaan van hoecke#38

Merged
sevaho merged 6 commits intomasterfrom
feature/CU-869byc3qm_Fix-SDK-openapi-types-being-optional-but-shown-as-any-of-variant_Sebastiaan-Van-Hoecke
Feb 6, 2026
Merged

Feature/cu 869byc3qm fix sdk openapi types being optional but shown as any of variant sebastiaan van hoecke#38
sevaho merged 6 commits intomasterfrom
feature/CU-869byc3qm_Fix-SDK-openapi-types-being-optional-but-shown-as-any-of-variant_Sebastiaan-Van-Hoecke

Conversation

@sevaho
Copy link
Contributor

@sevaho sevaho commented Feb 4, 2026

Fix issues with 'AnyOf' type generation for optional types and just render the actual type instead.

Summary by Sourcery

Adjust AsyncAPI schema generation to flatten optional types and add coverage ensuring optional fields are rendered as simple string types rather than anyOf variants.

Enhancements:

  • Override nullable schema handling to generate flat schemas for optional types instead of anyOf [type, null] structures.

Build:

  • Simplify the development Nix shell by removing Poetry and extra Python tooling dependencies.

Tests:

  • Add an AsyncAPI generation test verifying that various optional string fields are emitted as plain string types and that the schema retrieved over NATS matches the generated schema.

Summary by CodeRabbit

  • Bug Fixes

    • Improved handling of optional/nullable field types in generated API schemas for Pydantic v2 so optional types are represented inline rather than as anyOf([type, null]).
  • Chores

    • Simplified development environment by removing several unused tooling dependencies.
  • Tests

    • Added end-to-end tests validating optional and union type handling in schema generation and retrieval.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 4, 2026

Reviewer's Guide

Adjusts Pydantic V2 nullable schema handling so optional fields no longer generate anyOf[type, null] but instead emit just the underlying type, adds tests to validate AsyncAPI schema generation for various optional field patterns, and simplifies the Nix shell development environment dependencies.

Sequence diagram for nullable_schema override in optional field generation

sequenceDiagram
    participant Caller
    participant CompatJsonSchemaGenerator
    participant BaseJsonSchemaGenerator

    Caller->>CompatJsonSchemaGenerator: nullable_schema(schema)
    alt PYDANTIC_V2 is true and schema contains key schema
        CompatJsonSchemaGenerator->>CompatJsonSchemaGenerator: inner_schema = schema.get(schema)
        CompatJsonSchemaGenerator->>CompatJsonSchemaGenerator: generate_inner(inner_schema)
        CompatJsonSchemaGenerator-->>Caller: JsonSchemaValue without null anyOf
    else Fallback to base implementation
        CompatJsonSchemaGenerator->>BaseJsonSchemaGenerator: nullable_schema(schema)
        BaseJsonSchemaGenerator-->>CompatJsonSchemaGenerator: JsonSchemaValue (possibly anyOf[type, null])
        CompatJsonSchemaGenerator-->>Caller: JsonSchemaValue
    end
Loading

Class diagram for updated nullable_schema handling

classDiagram
    class BaseJsonSchemaGenerator {
        +nullable_schema(schema: JsonSchemaValue) JsonSchemaValue
    }

    class CompatJsonSchemaGenerator {
        +PYDANTIC_V2 bool
        +generate_inner(schema: JsonSchemaValue) JsonSchemaValue
        +nullable_schema(schema: JsonSchemaValue) JsonSchemaValue
    }

    BaseJsonSchemaGenerator <|-- CompatJsonSchemaGenerator

    class JsonSchemaValue {
    }

    CompatJsonSchemaGenerator : +nullable_schema(schema) JsonSchemaValue
    CompatJsonSchemaGenerator : when PYDANTIC_V2 and schema has key schema
    CompatJsonSchemaGenerator :   extract inner_schema
    CompatJsonSchemaGenerator :   return generate_inner(inner_schema)
    CompatJsonSchemaGenerator : otherwise call super.nullable_schema(schema)
Loading

File-Level Changes

Change Details Files
Customize nullable schema generation to flatten optional types instead of emitting anyOf with null.
  • Override nullable_schema to intercept generation of optional/nullable types.
  • When running under Pydantic V2, extract the inner schema from the nullable wrapper and delegate to generate_inner, effectively dropping the null variant.
  • Fall back to the base implementation for non-V2 environments or when no inner schema is present.
natsapi/_compat.py
Add AsyncAPI regression test to ensure optional model fields are rendered as simple string types in the generated schema and over NATS.
  • Define a User Pydantic model with mandatory and multiple syntactic variants of optional string fields, including default None and union-with-None forms.
  • Register a request handler using SubjectRouter that returns the User schema, generate the AsyncAPI schema, and assert each property’s type is 'string' regardless of optionality.
  • Issue a schema retrieval request via NATS and assert the retrieved schema matches the locally generated one.
tests/asyncapi/test_generation.py
Simplify the Nix development shell configuration by trimming Python tooling and Poetry overrides.
  • Remove Poetry override and the python311.withPackages development environment with language server and formatting tools.
  • Keep only core language/toolchain packages (python311, ruff, rustc, cargo) in the shell profile.
shell.nix

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai
Copy link

coderabbitai bot commented Feb 4, 2026

Warning

Rate limit exceeded

@sevaho has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 10 minutes and 27 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📝 Walkthrough

Walkthrough

Adds a Pydantic‑v2 aware override for nullable JSON schema generation that flattens Optional/Union nullable types, removes several development tool dependencies from shell.nix, and adds an async test validating optional/union schema generation and schema retrieval.

Changes

Cohort / File(s) Summary
Schema Generation Logic
natsapi/_compat.py
Added nullable_schema(self, schema: JsonSchemaValue) -> JsonSchemaValue to MyGenerateJsonSchema (public class). For Pydantic v2 it attempts to extract and return the inner schema for optional types instead of emitting an anyOf([... , null]) fallback; otherwise falls back to super().nullable_schema(...).
Environment Configuration
shell.nix
Removed development tool dependencies (e.g., ruff, rustc, cargo, and associated Python dev-tool blocks) from the Nix shell configuration.
Test Coverage
tests/asyncapi/test_generation.py
Added test_optional_types_are_generated_correctly(app: NatsAPI) async test validating OpenAPI/AsyncAPI schema output for various Optional/Union scenarios and verifying schema retrieval via RPC matches generated schema. Note: import extension and test appear duplicated in the file diff.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 I nudged the nulls and found what's core,
Pulled inner types from Pydantic's door.
Trimmed my shell of extra gear,
Ran tests that hop and check with cheer —
🥕 A tiny schema feast, encore!

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title is overly long, includes branch metadata and author name, and is unclear about the actual change being made. Simplify the title to focus on the core change, e.g., 'Fix optional types shown as anyOf in OpenAPI schema generation' or 'Flatten nullable schemas for optional Pydantic fields'.
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

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

✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/CU-869byc3qm_Fix-SDK-openapi-types-being-optional-but-shown-as-any-of-variant_Sebastiaan-Van-Hoecke

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

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

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

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location> `natsapi/_compat.py:239-247` </location>
<code_context>
         """
         return value
+
+    def nullable_schema(self, schema: JsonSchemaValue) -> JsonSchemaValue:
+        """
+        Override nullable schema generation to flatten optional types.
+        Instead of generating anyOf with [type, null], just return the type.
+        """
+        if PYDANTIC_V2:
+            # Extract the inner schema and generate it without the null variant
+            inner_schema = schema.get('schema')
+            if inner_schema:
+                return self.generate_inner(inner_schema)
+        return super().nullable_schema(schema)
</code_context>

<issue_to_address>
**suggestion:** Consider checking for the presence of the 'schema' key rather than its truthiness to avoid skipping valid but empty inner schemas.

Because `inner_schema = schema.get('schema')` is checked with `if inner_schema:`, an empty dict (or other falsy but valid value) will be treated as if there is no inner schema and will fall back to `super().nullable_schema(schema)`. If empty dicts are valid inner schemas, this creates inconsistent behavior. Using `if 'schema' in schema:` and then `self.generate_inner(schema['schema'])` will consistently apply the new behavior whenever an inner schema is present.
</issue_to_address>

### Comment 2
<location> `tests/asyncapi/test_generation.py:150-154` </location>
<code_context>
+    schema = app.asyncapi_schema
+
+    assert schema["components"]["schemas"]["User"]["properties"]["mandatory_property_1"]["type"] == "string"
+    assert schema["components"]["schemas"]["User"]["properties"]["optional_property_1"]["type"] == "string"
+    assert schema["components"]["schemas"]["User"]["properties"]["optional_property_2"]["type"] == "string"
+    assert schema["components"]["schemas"]["User"]["properties"]["optional_property_3"]["type"] == "string"
+    assert schema["components"]["schemas"]["User"]["properties"]["optional_property_4"]["type"] == "string"
+    assert schema["components"]["schemas"]["User"]["properties"]["optional_property_5"]["type"] == "string"
+    assert schema["components"]["schemas"]["User"]["properties"]["optional_property_6"]["type"] == "string"
+
</code_context>

<issue_to_address>
**suggestion (testing):** Assert explicitly that `anyOf` with a `null` schema is no longer generated for optional fields.

These assertions don’t actually verify the bug is fixed, because they would still pass if the field were wrapped in an `anyOf` that includes `"type": "string"`. Please also assert that `anyOf` is absent from the optional field schemas, e.g.:

```python
type_schema = schema["components"]["schemas"]["User"]["properties"]["optional_property_1"]
assert "anyOf" not in type_schema
assert type_schema.get("type") == "string"
```
(and similarly for a representative set of optional fields), so the test will fail if `anyOf` reappears.

```suggestion
    app.include_router(user_router)
    app.generate_asyncapi()
    schema = app.asyncapi_schema

    user_properties = schema["components"]["schemas"]["User"]["properties"]

    # Mandatory field should be a plain string type
    assert user_properties["mandatory_property_1"]["type"] == "string"

    # Optional fields should not be wrapped in anyOf and should have a plain string type
    optional_1_schema = user_properties["optional_property_1"]
    assert "anyOf" not in optional_1_schema
    assert optional_1_schema.get("type") == "string"

    optional_2_schema = user_properties["optional_property_2"]
    assert "anyOf" not in optional_2_schema
    assert optional_2_schema.get("type") == "string"

    optional_3_schema = user_properties["optional_property_3"]
    assert "anyOf" not in optional_3_schema
    assert optional_3_schema.get("type") == "string"

    optional_4_schema = user_properties["optional_property_4"]
    assert "anyOf" not in optional_4_schema
    assert optional_4_schema.get("type") == "string"

    optional_5_schema = user_properties["optional_property_5"]
    assert "anyOf" not in optional_5_schema
    assert optional_5_schema.get("type") == "string"

    optional_6_schema = user_properties["optional_property_6"]
    assert "anyOf" not in optional_6_schema
    assert optional_6_schema.get("type") == "string"
```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Copy link

@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

🤖 Fix all issues with AI agents
In `@tests/asyncapi/test_generation.py`:
- Around line 129-136: In the User model, fix the nullable type annotations:
change optional_property_6 from "str = None" to a nullable type such as
"Optional[str] = None" or "str | None = None" so the default None matches the
type, and simplify the redundant union on optional_property_4 (currently
"Optional[str] | None = None") to just "Optional[str] = None" (or "str | None =
None") to remove redundancy; update the annotations for optional_property_4 and
optional_property_6 in the User class accordingly.

@wegroupwolves wegroupwolves deleted a comment from sourcery-ai bot Feb 6, 2026
@sevaho sevaho merged commit 95a47ad into master Feb 6, 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