Skip to content

fern check passes invalid inheritance causing broken Python and unsafe Rust code #11092

@1e0cf

Description

@1e0cf

Which Fern component?

SDK Generator

How urgent is this?

P0 - Critical (Blocking work)

What's the issue?

Summary

I encountered a critical issue where fern check validates an OpenAPI schema as correct, but the generated code is either syntactically invalid (Python) or logically broken (Rust).

The Core Issue

The problem arises during the flattening of a schema that combines allOf inheritance with anyOf polymorphism, specifically when a property is defined in the base and re-defined in the polymorphic branches.

openapi: 3.0.0
info:
  title: Fern Duplicate Fields Repro
  version: 1.0.0
paths:
  /repro:
    post:
      operationId: repro
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/Root'
      responses:
        '200':
          description: OK

components:
  schemas:
    Root:
      allOf:
        - $ref: '#/components/schemas/Base'
        - $ref: '#/components/schemas/UnionWrapper'
      type: object

    Base:
      type: object
      properties:
        id:
          type: integer
          description: "ID from Base schema"

    UnionWrapper:
      anyOf:
        - $ref: '#/components/schemas/BranchWithSameId'
        - $ref: '#/components/schemas/BranchWithDifferentId'
      type: object

    BranchWithSameId:
      type: object
      properties:
        id:
          type: integer
          description: "ID from Branch (Same Type)"
        other_field:
          type: string

    BranchWithDifferentId:
      type: object
      properties:
        id:
          type: string
          description: "ID from Branch (Different Type)"
        unique_field:
          type: string

In the attached reproduction (repro_issue.yaml), the schema Root is defined using allOf to merge two components:

  1. Base: Defines a common property id (type: integer).
  2. UnionWrapper: Contains an anyOf list pointing to branches like BranchWithSameId and BranchWithDifferentId.

The Conflict:
Crucially, the branches inside UnionWrapper also define an id field. Fern fails to correctly merge, override, or deduplicate these fields during generation. This happens regardless of whether the types match or conflict:

  1. Same Type Scenario: Base has id: integer and BranchWithSameId has id: integer. Even though the types are compatible, Fern generates duplicate entries for id instead of merging them.
  2. Conflicting Type Scenario: Base has id: integer and BranchWithDifferentId has id: string (e.g., UUID). This is a structural conflict that fern check ignores entirely, leading to broken code generation.
Impact on Generated Code

Instead of failing fast or resolving the hierarchy, the generators output models with colliding properties:

  • Python: The generator produces code with duplicate dictionary keys or arguments (e.g., id=..., id=...), causing syntax errors or linter crashes (F601 Dictionary key literal "id" repeated).
  • Rust: The generator creates a struct with multiple fields mapping to the same JSON key. As seen in the output, Root contains both pub root_id (renamed to "id") and pub id (implicitly "id"). This compiles successfully but creates a runtime logic hazard. Serde behavior becomes ambiguous (duplicate keys in JSON payload), leading to silent data loss or deserialization errors depending on which field is processed last.
pub struct Root {
    /// ID from Branch (Different Type)
    #[serde(rename = "id")]
    #[serde(skip_serializing_if = "Option::is_none")]
    pub root_id: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub other_field: Option<String>,
    #[serde(skip_serializing_if = "Option::is_none")]
    pub unique_field: Option<String>,
    /// ID from Base schema
    #[serde(skip_serializing_if = "Option::is_none")]
    pub id: Option<String>,
}

Steps to reproduce:

  1. Initialize Fern project (or use an existing one).
  2. Create an OpenAPI specification file (e.g., repro_issue.yaml).
  3. Configure fern/generators.yml to point to this OpenAPI file.
api:
  specs:
    - openapi: repro_issue.yaml
default-group: local
groups:
  local:
    generators:
      - name: fernapi/fern-python-sdk
        version: 4.42.0
        output:
          location: local-file-system
          path: ./out/fern/python
      - name: fernapi/fern-rust-sdk
        version: 0.13.3
        output:
          location: local-file-system
          path: ./out/fern/rust
  1. Ensure fern check doesn't fail.
fern check
# [api]: ✓ All checks passed
  1. Run the generation command:
fern generate --local --log-level trace

Expected behavior:

fern check should detect the property collision between the allOf base schema and anyOf branches instead of passing silently.

I see two valid approaches for handling this:

  1. Type-Aware Resolution:

    • If types match: fern check should emit a warning regarding the duplicate field definition but proceed to generate valid code where the fields are merged into a single property.
    • If types differ: fern check must fail with an error, as the conflict cannot be resolved safely without manual intervention.
  2. Strict Validation:

    • fern check should always fail with an error (e.g., Object has multiple properties named "id"), regardless of whether the types match or not. This avoids ambiguity and forces the user to explicitily resolve the schema structure.

Actual behavior:

  • fern check passes
  • Python generator crashes during linting stage (ruff) with F601 Dictionary key literal "id" repeated
  • Rust generator produces unsafe code (contradicts the fail-fast principle)

Environment:

  • macOS 26.1
  • Node.js 25.2.1
  • podman instead of docker (v5.7.0, 8Gi, 4 CPU machine)

python_config.json
python_generator_out.txt
python_ir.json
rust_config.json
rust_generator_out.txt
rust_ir.json

Fern CLI & Generator Versions

Fern CLI version: 3.4.2
SDK Generator versions:

  • fernapi/fern-python-sdk v4.42.0
  • fernapi/fern-rust-sdk v0.13.3

Workaround

No response

Are you interested in contributing a fix?

No

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions