Skip to content
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
5c17e02
chore(seed): add allOf composition test fixtures
jsklan Apr 9, 2026
2222762
chore(internal): add allOf debug pipeline script and IR snapshots
jsklan Apr 10, 2026
30e1997
test(cli): add assertion-based allOf tests reproducing SPS Commerce bugs
jsklan Apr 10, 2026
6425f88
fix(cli): resolve allOf composition bugs in V3 OpenAPI importer
devin-ai-integration[bot] Apr 10, 2026
197e1c8
fix: resolve biome noNonNullAssertion lint errors in SchemaOrReferenc…
devin-ai-integration[bot] Apr 10, 2026
f35278e
fix: add biome-ignore for pre-existing noNonNullAssertion in test file
devin-ai-integration[bot] Apr 10, 2026
77c5f8f
fix: update v3-sdks snapshots for allOf merge changes
devin-ai-integration[bot] Apr 10, 2026
11d04fc
fix: update convertIRtoJsonSchema snapshots for allof/allof-inline
devin-ai-integration[bot] Apr 10, 2026
252c461
update config
jsklan Apr 10, 2026
4b0f3b1
Add case 6
jsklan Apr 10, 2026
fa8952f
seed outputs
jsklan Apr 10, 2026
edcfe09
Automated update of seed files
jsklan Apr 10, 2026
4471aa4
Merge main into jsklan/allof-openapi-import to resolve conflicts
devin-ai-integration[bot] Apr 10, 2026
ba0d10a
fix: update convertIRtoJsonSchema snapshots for new allof types
devin-ai-integration[bot] Apr 10, 2026
aca9275
fix: thread visitedRefs across recursive SchemaConverter calls for cr…
devin-ai-integration[bot] Apr 10, 2026
a83524d
fix: format SchemaConverter.ts constructor for biome
devin-ai-integration[bot] Apr 10, 2026
8e673f2
fix: update ir-generator-tests test-definitions for allof-inline
devin-ai-integration[bot] Apr 10, 2026
a042fe6
fix: update ir-generator-tests test-definitions for allof
devin-ai-integration[bot] Apr 10, 2026
0c05937
fix: deduplicate merged allOf refs and guard composition-keyword shor…
jsklan Apr 10, 2026
f121831
fix: guard single-element allOf cycles and add versions.yml entry
jsklan Apr 10, 2026
5456b3c
Merge remote-tracking branch 'origin/main' into jsklan/allof-openapi-…
jsklan Apr 10, 2026
d7d470c
Merge main into jsklan/allof-openapi-import to resolve versions.yml c…
devin-ai-integration[bot] Apr 10, 2026
47d9f75
nits
jsklan Apr 10, 2026
ecb4507
fix(cli): address review feedback on allOf composition
jsklan Apr 10, 2026
53001ff
fix(cli): address remaining review comments on allOf composition
devin-ai-integration[bot] Apr 13, 2026
dabfa9e
fix(cli): separate resolvedRefs concerns and add format keyword guard
devin-ai-integration[bot] Apr 13, 2026
f266891
style: fix biome formatting for inline format guard
devin-ai-integration[bot] Apr 13, 2026
386686c
fix(cli): preserve outer schema metadata in shouldMergeAllOf path
devin-ai-integration[bot] Apr 13, 2026
b32cbd5
refactor(cli): replace lodash.mergeWith with per-key allOf schema merge
jsklan Apr 14, 2026
39e188a
Merge remote-tracking branch 'origin/main' into jsklan/allof-openapi-…
jsklan Apr 14, 2026
4bec8ca
fix(cli): fix type errors in allOf schema merge and eliminate `as any…
jsklan Apr 15, 2026
b67826e
fix(cli): harden allOf merge pipeline against $ref leaks, nested flat…
jsklan Apr 15, 2026
846d282
Merge remote-tracking branch 'origin/main' into jsklan/allof-openapi-…
jsklan Apr 15, 2026
fc0680b
Revert seed output changes to reduce diff
jsklan Apr 15, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
schema: OpenAPIV3_1.SchemaObject;
inlined?: boolean;
nameOverride?: string;
visitedRefs?: Set<string>;
}

export interface ConvertedSchema {
Comment thread
claude[bot] marked this conversation as resolved.
Expand All @@ -51,13 +52,23 @@
private readonly inlined: boolean;
private readonly audiences: string[];
private readonly nameOverride?: string;

constructor({ context, breadcrumbs, schema, id, inlined = false, nameOverride }: SchemaConverter.Args) {
private readonly visitedRefs: Set<string>;

constructor({
context,
breadcrumbs,
schema,
id,
inlined = false,
nameOverride,
visitedRefs
}: SchemaConverter.Args) {
super({ context, breadcrumbs });
this.schema = schema;
this.id = id;
this.inlined = inlined;
this.nameOverride = nameOverride;
this.visitedRefs = visitedRefs ?? new Set<string>();
this.audiences =
this.context.getAudiences({
operation: this.schema,
Expand Down Expand Up @@ -179,7 +190,8 @@
breadcrumbs: [...this.breadcrumbs, "allOf", "0"],
schema: allOfSchema,
id: this.id,
inlined: true
inlined: true,
visitedRefs: this.visitedRefs
});

const allOfResult = allOfConverter.convert();
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
claude[bot] marked this conversation as resolved.
Comment thread
jsklan marked this conversation as resolved.
Expand All @@ -197,17 +209,36 @@

if (shouldMergeAllOf) {
let mergedSchema: Record<string, unknown> = {};
const resolvedRefs = new Set<string>(this.visitedRefs);
let hasCycle = false;
for (const allOfSchema of this.schema.allOf ?? []) {
let schemaToMerge: OpenAPIV3_1.SchemaObject;

if (this.context.isReferenceObject(allOfSchema)) {
return undefined;
const refPath = allOfSchema.$ref;
if (resolvedRefs.has(refPath)) {
hasCycle = true;
break;
}
resolvedRefs.add(refPath);
Comment thread
claude[bot] marked this conversation as resolved.
Outdated

const resolved = this.context.resolveMaybeReference<OpenAPIV3_1.SchemaObject>({
schemaOrReference: allOfSchema,
breadcrumbs: this.breadcrumbs
});
if (resolved == null) {
return undefined;
Comment thread
claude[bot] marked this conversation as resolved.
}
schemaToMerge = resolved;
} else {
schemaToMerge = allOfSchema;
}

// Handle bare oneOf/anyOf elements used for mutual exclusion patterns
// (e.g., oneOf with variants containing `not: {}` properties).
// Flatten variant properties into the merged schema as optional properties.
const variants =
(allOfSchema as OpenAPIV3_1.SchemaObject).oneOf ?? (allOfSchema as OpenAPIV3_1.SchemaObject).anyOf;
if (variants != null && allOfSchema.type == null && allOfSchema.properties == null) {
const variants = schemaToMerge.oneOf ?? schemaToMerge.anyOf;
if (variants != null && schemaToMerge.type == null && schemaToMerge.properties == null) {
const flattenedProperties: Record<string, unknown> = {};
for (const variantSchemaOrRef of variants) {
const variantSchema = this.context.isReferenceObject(variantSchemaOrRef)
Comment thread
claude[bot] marked this conversation as resolved.
Expand Down Expand Up @@ -243,26 +274,32 @@
continue;
}

mergedSchema = mergeWith(mergedSchema, allOfSchema, (objValue, srcValue) => {
if (srcValue === allOfSchema) {
mergedSchema = mergeWith(mergedSchema, schemaToMerge, (objValue, srcValue) => {
if (srcValue === schemaToMerge) {
return objValue;
}
if (Array.isArray(objValue) && Array.isArray(srcValue)) {
return [...objValue, ...srcValue];
}
return undefined;
});
}

// If a circular reference was detected, fall back to the ObjectSchemaConverter path
if (hasCycle) {
return undefined;
}

const mergedConverter = new SchemaConverter({
context: this.context,
breadcrumbs: this.breadcrumbs,
schema: mergedSchema,
id: this.id,
inlined: true
inlined: true,
visitedRefs: resolvedRefs
});
return mergedConverter.convert();
Comment thread
jsklan marked this conversation as resolved.
}

Check failure on line 302 in packages/cli/api-importers/v3-importer-commons/src/converters/schema/SchemaConverter.ts

View check run for this annotation

Claude / Claude Code Review

Diamond inheritance causes false-positive cycle detection via allOf array concatenation

The `mergeWith` array-concatenation customizer in the `shouldMergeAllOf` path (SchemaConverter.ts:285-302) creates duplicate `$ref` entries for diamond-shaped inheritance (A → B, C; B, C → D), causing the cycle detector to flag the second occurrence of `$ref:D` in the concatenated `allOf` array as a false-positive cycle and fall back to `ObjectSchemaConverter`, silently dropping D's properties. To fix, deduplicate `$ref` entries in the merged `allOf` array before passing it to `mergedConverter`,
Comment thread
jsklan marked this conversation as resolved.
Outdated

return undefined;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -82,26 +82,60 @@
}

private maybeConvertSingularAllOfReferenceObject(): SchemaOrReferenceConverter.Output | undefined {
if (
this.context.isReferenceObject(this.schemaOrReference) ||
this.schemaOrReference.allOf == null ||
this.schemaOrReference.allOf.length !== 1
) {
if (this.context.isReferenceObject(this.schemaOrReference) || this.schemaOrReference.allOf == null) {
return undefined;
}
const allOfReference = this.schemaOrReference.allOf[0];
if (this.context.isReferenceObject(allOfReference)) {
const response = this.context.convertReferenceToTypeReference({
reference: allOfReference,

// Single $ref allOf — convert directly as a type reference
if (this.schemaOrReference.allOf.length === 1) {
const allOfReference = this.schemaOrReference.allOf[0];
if (this.context.isReferenceObject(allOfReference)) {
const response = this.context.convertReferenceToTypeReference({
reference: allOfReference,
breadcrumbs: this.breadcrumbs
});
if (response.ok) {
return {
type: this.wrapTypeReference(response.reference),
inlinedTypes: {}
};
}
}
return undefined;
}

// When allOf has exactly one $ref to a non-object type (e.g. enum) and the
// remaining elements are inline primitive constraints (no properties, no enum,
// no composition keywords), reference the $ref type directly instead of
// creating a synthetic merged copy.
const refElements = this.schemaOrReference.allOf.filter((s) => this.context.isReferenceObject(s));
const inlineElements = this.schemaOrReference.allOf.filter(
(s) => !this.context.isReferenceObject(s)
) as OpenAPIV3_1.SchemaObject[];
const singleRef = refElements.length === 1 ? refElements[0] : undefined;
Comment thread
claude[bot] marked this conversation as resolved.

if (
singleRef != null &&
inlineElements.every((s) => !s.properties && !s.enum && !s.oneOf && !s.anyOf && !s.allOf)
) {
const resolved = this.context.resolveMaybeReference<OpenAPIV3_1.SchemaObject>({
schemaOrReference: singleRef,
breadcrumbs: this.breadcrumbs
});
if (response.ok) {
return {
type: this.wrapTypeReference(response.reference),
inlinedTypes: {}
};
if (resolved != null && resolved.type !== "object" && !resolved.properties) {
const response = this.context.convertReferenceToTypeReference({
reference: singleRef as OpenAPIV3_1.ReferenceObject,
breadcrumbs: this.breadcrumbs
});
if (response.ok) {
return {
type: this.wrapTypeReference(response.reference),
inlinedTypes: {}
};
}
Comment thread
jsklan marked this conversation as resolved.
}

Check failure on line 136 in packages/cli/api-importers/v3-importer-commons/src/converters/schema/SchemaOrReferenceConverter.ts

View check run for this annotation

Claude / Claude Code Review

maybeConvertSingularAllOfReferenceObject misses composition types in resolved schema guard

The guard in `maybeConvertSingularAllOfReferenceObject` at line 125 checks only `resolved.type \!== "object" && \!resolved.properties` before returning a direct type reference for the `$ref`, but fails to exclude schemas defined via composition keywords (`oneOf`, `anyOf`, `allOf`). For any API with a property typed as `allOf: [$ref: CompositionType, {inline constraint}]` where `CompositionType` is defined via `oneOf`/`anyOf`/`allOf`, the inline constraint elements will be silently discarded and
Comment thread
jsklan marked this conversation as resolved.
}
Comment thread
jsklan marked this conversation as resolved.

return undefined;
}

Expand Down
Loading
Loading