springdoc-openapi icon indicating copy to clipboard operation
springdoc-openapi copied to clipboard

Incorrect schema/example with `@EmbeddedId` + relations (Spring Data REST)

Open dpkass opened this issue 2 months ago • 0 comments

Summary
With JPA @EmbeddedId/@MapsId join entities and Spring Data REST, springdoc generates recursively nested examples instead of compact, reference-like shapes. This misleads client generators and bloats the OpenAPI.

Environment

  • Spring Boot + Spring Data JPA + Spring Data REST (HAL)
  • springdoc: 2.8.14

Minimal model (flattened for clarity)

@Embeddable
class GoalInitiativeImpactId { Long goalId; Long initiativeId; }

@Entity
class Goal {
  @Id @GeneratedValue Long id;
  String name;
  // other fields

  @OneToMany(mappedBy = "goal", cascade = CascadeType.ALL, orphanRemoval = true)
  Set<InitiativeImpactOnGoal> impactsByInitiatives = new HashSet<>();
}

@Entity
class Initiative {
  @Id @GeneratedValue Long id;
  String name;
  // other fields
}

@Entity
class InitiativeImpactOnGoal {
  @EmbeddedId GoalInitiativeImpactId id;
  @ManyToOne @MapsId("goalId") Goal goal;
  @ManyToOne @MapsId("initiativeId") Initiative initiative;
  @Enumerated(EnumType.STRING) ImpactLevel impactLevel;
}

Repositories are exposed via Spring Data REST.


Actual (runtime showcase) — request & response

Request
GET /goals/1

Response (trimmed)

{
  "id": 1,
  "name": { "en-US": "string" },
  "impactsByInitiatives": [
    {
      "impactLevel": "TRIVIAL",
      "_links": {
        "goal": { "href": "/goals/1" },
        "initiative": { "href": "/initiatives/42" }
      }
    }
  ],
  "_links": {
    "self": { "href": "/goals/1" },
    "indicators": { "href": "/goals/1/indicators" },
    "parent": { "href": "/goals/1/parent" }
  }
}

The join entity appears as an item with impactLevel + HAL links.


Generated by springdoc (Swagger UI example) — problematic (trimmed)

{
  "id": 1,
  "name": { "en-US": "string" },
  "description": { "en-US": "string" },
  "workflowStatus": "DRAFT",
  "impactsByInitiatives": [
    {
      "id": { "goalId": 1, "initiativeId": 42 },
      "goal": {
        "id": 1,
        "name": { "en-US": "string" },
        "parent": {
          "id": 7,
          "parent": { /* … nests again … */ }
        },
        "initiatives": [
          {
            "id": 42,
            "name": "string",
            "payments": [ { /* … */ } ],
            "progressLog": [ { /* … */ } ]
          }
        ],
        "children": [ { /* … */ } ]
      },
      "initiative": {
        "id": 42,
        "name": "string",
        "payments": [ { /* … */ } ],
        "progressLog": [ { /* … */ } ]
      },
      "impactLevel": "TRIVIAL"
    }
  ],
  "_links": {
    "self": { "href": "/goals/1" }
  }
}

Issue: deep recursion and full entity expansion inside the join entity, unlike the HAL representation actually returned at runtime.


Expected

Reflect the actual shape for read operations. For write ops we can just use READ_ONLY ourselves.


Workarounds (short)

  • Expose DTOs/projections for REST (flatten associations).
  • Consider a surrogate @Id on the join entity if OpenAPI consumers choke on composite keys.

dpkass avatar Nov 10 '25 23:11 dpkass