Skip to content

Commit d6b9003

Browse files
fix(java): generate standalone files for orphaned inline types in endpoint responses (#16242)
* fix(java): generate standalone files for inline types only referenced from endpoint responses When enable-inline-types is enabled, inline types that are only referenced from endpoint response types (not from other type declarations) were skipped during file generation. The generator assumed all inline types would be nested inside parent types, but map value types in endpoint responses have no parent type to nest into. Added BFS reachability analysis from non-inline types to determine which inline types will actually be nested. Only those are skipped; orphaned inline types are now generated as standalone files. Added test case to java-inline-types fixture with a map response endpoint. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: apply spotless formatting Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: update ir-to-jsonschema snapshot for MapResponseValue Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix: prevent duplicate standalone files for inline types nested inside orphaned inline types Addresses Devin Review finding: when an orphaned inline type (only referenced from endpoints) itself references other inline types, those child types should be nested inside the orphan, not generated as redundant standalone files. Added second pass in computeNestedInlineTypeIds() to mark inline types referenced from orphaned inline types as nested. Also added test case with MapResponseValueNested inside MapResponseValue to validate. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * chore: regenerate IR test definitions for java-inline-types Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * fix(java): remove unsafe second-pass suppression of orphan-referenced inline types The second pass unconditionally marked all inline types referenced by orphans as nested, which fails for: 1. Self-referencing orphans (e.g. Tree { children: list<Tree> }) — the type appears in its own getReferencedTypes() (transitive closure) and gets suppressed with no parent to nest into. 2. Inline types shared between an orphan parent and an endpoint — the type gets suppressed but the endpoint client still emits a top-level import. Fix: remove the second pass entirely. Orphan inline types that reference other inline types will produce both a standalone file and a nested class for the child — this duplication is harmless and always correct. Also adds test fixtures for shared-inline-type (endpoint + orphan parent) edge case. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: add JSON schema snapshots for SharedChildType and OrphanParentWithSharedChild Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> * test: regenerate IR and dynamic-snippets test definitions for java-inline-types Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --------- Co-authored-by: cade <info@buildwithfern.com> Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
1 parent 6e3c48c commit d6b9003

56 files changed

Lines changed: 14191 additions & 1315 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

generators/java/generator-utils/src/main/java/com/fern/java/generators/TypesGenerator.java

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,11 @@
2525
import com.fern.java.output.GeneratedJavaInterface;
2626
import com.palantir.common.streams.KeyedStream;
2727
import com.squareup.javapoet.ClassName;
28+
import java.util.HashSet;
29+
import java.util.LinkedList;
2830
import java.util.Map;
2931
import java.util.Optional;
32+
import java.util.Queue;
3033
import java.util.Set;
3134
import java.util.function.Function;
3235
import java.util.stream.Collectors;
@@ -44,10 +47,16 @@ public TypesGenerator(AbstractGeneratorContext<?, ?> generatorContext) {
4447

4548
public Result generateFiles() {
4649
Map<TypeId, GeneratedJavaInterface> generatedInterfaces = getGeneratedInterfaces(generatorContext);
50+
51+
Set<TypeId> nestedInlineTypeIds =
52+
generatorContext.getCustomConfig().enableInlineTypes() ? computeNestedInlineTypeIds() : Set.of();
53+
4754
Map<TypeId, GeneratedJavaFile> generatedTypes = KeyedStream.stream(typeDeclarations)
4855
.map(typeDeclaration -> {
4956
if (generatorContext.getCustomConfig().enableInlineTypes()
50-
&& typeDeclaration.getInline().orElse(false)) {
57+
&& typeDeclaration.getInline().orElse(false)
58+
&& nestedInlineTypeIds.contains(
59+
typeDeclaration.getName().getTypeId())) {
5160
return Optional.<GeneratedJavaFile>empty();
5261
}
5362

@@ -71,6 +80,46 @@ public Result generateFiles() {
7180
return new Result(generatedInterfaces, generatedTypes);
7281
}
7382

83+
/**
84+
* Computes the set of inline TypeIds that are reachable from at least one non-inline type declaration. These inline
85+
* types will be generated as nested classes inside their parent types and should be skipped during standalone file
86+
* generation.
87+
*
88+
* <p>Inline types that are NOT in this set (e.g., those only referenced from endpoint response types) must still be
89+
* generated as standalone files. Note: orphaned inline types that reference other inline types will produce both a
90+
* standalone file and a nested class for the child type. This duplication is intentional — suppressing the
91+
* standalone file is unsafe because {@code getReferencedTypes()} is the transitive closure and cannot distinguish
92+
* direct inline children from self-references, mutually-recursive types, or types shared with endpoints.
93+
*/
94+
private Set<TypeId> computeNestedInlineTypeIds() {
95+
Set<TypeId> nestedInlineTypeIds = new HashSet<>();
96+
Queue<TypeId> queue = new LinkedList<>();
97+
98+
for (Map.Entry<TypeId, TypeDeclaration> entry : typeDeclarations.entrySet()) {
99+
if (!entry.getValue().getInline().orElse(false)) {
100+
queue.add(entry.getKey());
101+
}
102+
}
103+
104+
while (!queue.isEmpty()) {
105+
TypeId current = queue.poll();
106+
TypeDeclaration currentDecl = typeDeclarations.get(current);
107+
if (currentDecl == null) {
108+
continue;
109+
}
110+
for (TypeId referencedId : currentDecl.getReferencedTypes()) {
111+
TypeDeclaration referencedDecl = typeDeclarations.get(referencedId);
112+
if (referencedDecl != null
113+
&& referencedDecl.getInline().orElse(false)
114+
&& nestedInlineTypeIds.add(referencedId)) {
115+
queue.add(referencedId);
116+
}
117+
}
118+
}
119+
120+
return nestedInlineTypeIds;
121+
}
122+
74123
public static Map<TypeId, GeneratedJavaInterface> getGeneratedInterfaces(
75124
AbstractGeneratorContext<?, ?> generatorContext) {
76125
Set<TypeId> interfaceCandidates = generatorContext.getInterfaceIds();
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
- summary: |
2+
Fix inline types referenced only from endpoint responses (e.g., map value
3+
types) not being generated as standalone files when `enable-inline-types`
4+
is enabled.
5+
type: fix
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"name": {
5+
"type": "string"
6+
},
7+
"description": {
8+
"type": "string"
9+
},
10+
"nested": {
11+
"oneOf": [
12+
{
13+
"$ref": "#/definitions/MapResponseValueNested"
14+
},
15+
{
16+
"type": "null"
17+
}
18+
]
19+
}
20+
},
21+
"required": [
22+
"name",
23+
"description"
24+
],
25+
"additionalProperties": false,
26+
"definitions": {
27+
"MapResponseValueNested": {
28+
"type": "object",
29+
"properties": {
30+
"key": {
31+
"type": "string"
32+
},
33+
"value": {
34+
"type": "string"
35+
}
36+
},
37+
"required": [
38+
"key",
39+
"value"
40+
],
41+
"additionalProperties": false
42+
}
43+
}
44+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"key": {
5+
"type": "string"
6+
},
7+
"value": {
8+
"type": "string"
9+
}
10+
},
11+
"required": [
12+
"key",
13+
"value"
14+
],
15+
"additionalProperties": false,
16+
"definitions": {}
17+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"label": {
5+
"type": "string"
6+
},
7+
"child": {
8+
"oneOf": [
9+
{
10+
"$ref": "#/definitions/SharedChildType"
11+
},
12+
{
13+
"type": "null"
14+
}
15+
]
16+
}
17+
},
18+
"required": [
19+
"label"
20+
],
21+
"additionalProperties": false,
22+
"definitions": {
23+
"SharedChildType": {
24+
"type": "object",
25+
"properties": {
26+
"name": {
27+
"type": "string"
28+
},
29+
"detail": {
30+
"type": "string"
31+
}
32+
},
33+
"required": [
34+
"name",
35+
"detail"
36+
],
37+
"additionalProperties": false
38+
}
39+
}
40+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"type": "object",
3+
"properties": {
4+
"name": {
5+
"type": "string"
6+
},
7+
"detail": {
8+
"type": "string"
9+
}
10+
},
11+
"required": [
12+
"name",
13+
"detail"
14+
],
15+
"additionalProperties": false,
16+
"definitions": {}
17+
}

0 commit comments

Comments
 (0)