dotCMS / dotCMS/core

[DEFECT] Reverse side of a relationship between two content types is omitted at depth >= 2 in the REST Content API

Open
#37,470 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

OKR : Customer Support Team : Maintenance Type : Defect
Dominant language
Java
Stars
970
Forks
486
Avg merge
3d 33m
Merged PRs (30d)
170

Description

Problem Statement

When two content types are related to each other and both sides of that single relationship are modelled as relationship fields, the REST Content API omits the second side entirely once the first has been traversed.

ContentHelper.addRelationshipsToJSON maintains an addedRelationships set. If a relationship has already been traversed on the current path, the loop hits continue (dotCMS/src/main/java/com/dotcms/rest/ContentHelper.java:586), which skips the jsonObject.put(field.variable(), ...) call. The field key is therefore absent from the JSON — not null, and not an empty array — so consumers get no signal that anything was suppressed.

Whichever side is traversed first "claims" the relationship and the reverse side is dropped one level down. A relationship field pointing at a different content type is unaffected, because it is a separate relationship and never already-visited. That asymmetry makes the behaviour very hard to diagnose: two relationship fields on the same content type, configured identically, behave differently.

This is the same defect class as #19796 ("depth>=3 doesn't work for self-related content"), which was labelled Type : Defect, QA'd, and shipped in 21.03 / 5.3.8.4 / 5.2.8.4. That fix (commit cd2e2f3f17) carved self-relationships out of the guard:

if (!relationship.getParentStructureInode().equals(relationship.getChildStructureInode())) {
    addedRelationships.add(relationship);
}

That carve-out only covers same-parent-and-child relationships. The two-type case is not covered, so the same user-visible symptom remains. The guard itself dates to #9411 / PR #15970 (2019), introduced alongside depth support to avoid repeated DB hits.

Impact: a customer on Evergreen hit this in production and spent time investigating their content model, because the field configuration looks correct and the failure is silent. GraphQL is unaffected — it resolves each relationship field independently — so the workaround is to move the query to GraphQL, or to query the second content type directly so the wanted field sits at level 1.

Steps to Reproduce

Video not applicable — this is a JSON response shape issue, reproducible with two API calls.

  1. Create content type TypeA and content type TypeB.
  2. On TypeB, add a relationship field bToA pointing at TypeA (One to many).
  3. On TypeA, add a relationship field aToB that references the same relationship (i.e. pick the existing TypeB.bToA relationship rather than creating a new one), so aToB and bToA are the two sides of one relationship.
  4. On TypeB, add a second relationship field bToC pointing at a different content type TypeC, for contrast.
  5. Create content: one TypeB instance related to three TypeA instances and one TypeC instance; relate one TypeA instance back to that TypeB instance via aToB.
  6. Query the TypeA contentlet with depth 3:
    GET /api/v1/content/{typeA-identifier}?depth=3

Expected: the nested TypeB object includes bToA (the three TypeA items) and bToC.

Actual: the nested TypeB object includes bToC but bToA is absent from the JSON entirely. Confirmed on 12 consecutive requests immediately after a full cache flush — the result is deterministic, not intermittent.

  1. Mirror case, which demonstrates the mechanism: query the TypeB contentlet with depth 3:
    GET /api/v1/content/{typeB-identifier}?depth=3

Actual: bToA is now returned correctly with all three items, but each nested TypeA object has aToB absent instead. Whichever side is traversed first is the one returned.

  1. For contrast, the same traversal via GraphQL (POST /api/v1/graphql) returns both bToA and bToC correctly and consistently.
Acceptance Criteria
  • When both sides of a single relationship between two different content types are modelled as fields, traversing from either side returns the reverse-side field rather than omitting it.
  • Cycle safety is preserved — the fix must not reintroduce unbounded expansion for cyclical relationship graphs (see #9411, and the story-block/relationship expansion concerns raised previously).
  • Behaviour is consistent with the self-relationship handling already added by #19796.
  • If a relationship field is deliberately not expanded at a given depth, the response is explicit about it (e.g. identifiers, or an empty array) rather than omitting the key with no signal.
  • Relationship traversal semantics for the depth parameter are documented — current docs do not cover depth traversal behaviour, cycles, or the 0–3 cap. This gap was raised previously in support and never actioned.
  • Regression test covering the two-type both-sides-modelled case, alongside the existing self-relationship coverage.
dotCMS Version

Reproduced on 26.08.19-04 (Evergreen, Cloud). Code path is present on main; the addedRelationships guard has existed since 2019 and the relevant lines were verified byte-for-byte at tag v26.08.19-04.

Severity

High - Major functionality broken

Links
  • Freshdesk ticket #39249
  • Precedent: #19796 (same defect class, self-relationships only), fix commit cd2e2f3f17
  • Origin of the guard: #9411 / PR #15970, commit 83760aa6d8
  • Related: #36012 (cached contentlet mutated during relationship hydration — adjacent, not the cause here)
Open question for engineering

@erickgonzalez — you authored the #19796 fix. Two candidate approaches, and we'd value your read on which is safer:

  1. Extend the existing carve-out so the reverse side of a shared two-type relationship is also re-traversable (narrower, mirrors #19796).
  2. Change the guard so a suppressed relationship field still emits identifiers or an empty array instead of dropping the key (broader, fixes the silent-failure ergonomics for every case).

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in dotCMS/src/main/java/com/dotcms/rest/ContentHelper.java around line 586 and trace how addedRelationships affects relationship fields during depth traversal. Compare the existing self-relationship handling with the two-content-type reproduction, then inspect the existing self-relationship regression coverage. Done means reverse-side fields are present without breaking cycle safety, with regression coverage and depth behavior documented.

Written by the indexing model from the issue text.

Assessment

Tech stack
graphql, java
Domain
api, backend
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.