From 0bab8aaef9d6a4c29320e95e1e65b7d1b08a79ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Mon, 1 Jun 2026 08:28:11 +0300 Subject: [PATCH 1/2] Stop boxing types that recurse only through an array or dictionary Arrays and dictionaries already break the reference cycle, so a type that reaches itself only through one of them does not need CopyOnWriteBox storage. The recursion detector now skips array and dictionary references when collecting cycle-forming edges, which matches the behavior described in Supporting-recursive-types.md. Direct and optional self-references are still boxed. Closes #682 --- .../DeclarationRecursionDetector.swift | 10 +-- .../SnippetBasedReferenceTests.swift | 68 +++++++++++++++++++ 2 files changed, 74 insertions(+), 4 deletions(-) diff --git a/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift b/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift index 93c14bac7..4e6414da7 100644 --- a/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift +++ b/Sources/_OpenAPIGeneratorCore/Translator/Recursion/DeclarationRecursionDetector.swift @@ -155,13 +155,15 @@ fileprivate extension Array where Element == String { extension ExistingTypeDescription { - /// The name in the `Components.Schemas.` namespace, if the type can appear - /// there. Nil otherwise. + /// The name in the `Components.Schemas.` namespace this type references + /// without an intervening array or dictionary, if any. Nil otherwise. var referencedSchemaComponentName: String? { switch self { case .member(let components): return components.nameIfTopLevelSchemaComponent - case .array(let desc), .dictionaryValue(let desc), .any(let desc), .optional(let desc): - return desc.referencedSchemaComponentName + case .any(let desc), .optional(let desc): return desc.referencedSchemaComponentName + // Arrays and dictionaries provide heap indirection, so a reference reached + // only through them cannot form a value-type cycle that needs boxing. + case .array, .dictionaryValue: return nil case .generic: return nil } } diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index 4a08b8ed0..d72bb0dc3 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1617,6 +1617,74 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasRecursive_throughArray() throws { + try self.assertSchemasTranslation( + """ + schemas: + Node: + type: object + properties: + children: + type: array + items: + $ref: '#/components/schemas/Node' + """, + """ + public enum Schemas { + public struct Node: Codable, Hashable, Sendable { + public var children: [Components.Schemas.Node]? + public init(children: [Components.Schemas.Node]? = nil) { + self.children = children + } + public enum CodingKeys: String, CodingKey { + case children + } + } + } + """ + ) + } + + func testComponentsSchemasRecursive_throughDictionary() throws { + try self.assertSchemasTranslation( + """ + schemas: + Node: + type: object + properties: + children: + type: object + additionalProperties: + $ref: '#/components/schemas/Node' + """, + """ + public enum Schemas { + public struct Node: Codable, Hashable, Sendable { + public struct childrenPayload: Codable, Hashable, Sendable { + public var additionalProperties: [String: Components.Schemas.Node] + public init(additionalProperties: [String: Components.Schemas.Node] = .init()) { + self.additionalProperties = additionalProperties + } + public init(from decoder: any Swift.Decoder) throws { + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: []) + } + public func encode(to encoder: any Swift.Encoder) throws { + try encoder.encodeAdditionalProperties(additionalProperties) + } + } + public var children: Components.Schemas.Node.childrenPayload? + public init(children: Components.Schemas.Node.childrenPayload? = nil) { + self.children = children + } + public enum CodingKeys: String, CodingKey { + case children + } + } + } + """ + ) + } + func testComponentsSchemasRecursive_objectNested() throws { try self.assertSchemasTranslation( """ From 20037e98ee2bd1f59c8c29c46d31760d64346c28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ey=C3=BCp=20Can=20Akman?= Date: Thu, 2 Jul 2026 19:57:51 +0300 Subject: [PATCH 2/2] Add tests for types that recurse through a direct ref and a container --- .../SnippetBasedReferenceTests.swift | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift index d72bb0dc3..6ea8d60b6 100644 --- a/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift +++ b/Tests/OpenAPIGeneratorReferenceTests/SnippetBasedReferenceTests.swift @@ -1685,6 +1685,172 @@ final class SnippetBasedReferenceTests: XCTestCase { ) } + func testComponentsSchemasRecursive_directAndThroughArray() throws { + try self.assertSchemasTranslation( + """ + schemas: + Node: + type: object + properties: + parent: + $ref: '#/components/schemas/Node' + children: + type: array + items: + $ref: '#/components/schemas/Node' + """, + """ + public enum Schemas { + public struct Node: Codable, Hashable, Sendable { + public var parent: Components.Schemas.Node? { + get { + self.storage.value.parent + } + _modify { + yield &self.storage.value.parent + } + } + public var children: [Components.Schemas.Node]? { + get { + self.storage.value.children + } + _modify { + yield &self.storage.value.children + } + } + public init( + parent: Components.Schemas.Node? = nil, + children: [Components.Schemas.Node]? = nil + ) { + self.storage = .init(value: .init( + parent: parent, + children: children + )) + } + public enum CodingKeys: String, CodingKey { + case parent + case children + } + public init(from decoder: any Swift.Decoder) throws { + self.storage = try .init(from: decoder) + } + public func encode(to encoder: any Swift.Encoder) throws { + try self.storage.encode(to: encoder) + } + private var storage: OpenAPIRuntime.CopyOnWriteBox + private struct Storage: Codable, Hashable, Sendable { + var parent: Components.Schemas.Node? + var children: [Components.Schemas.Node]? + init( + parent: Components.Schemas.Node? = nil, + children: [Components.Schemas.Node]? = nil + ) { + self.parent = parent + self.children = children + } + typealias CodingKeys = Components.Schemas.Node.CodingKeys + } + } + } + """ + ) + } + + func testComponentsSchemasRecursive_directAndThroughDictionary() throws { + try self.assertSchemasTranslation( + """ + schemas: + Node: + type: object + properties: + parent: + $ref: '#/components/schemas/Node' + children: + type: object + additionalProperties: + $ref: '#/components/schemas/Node' + """, + """ + public enum Schemas { + public struct Node: Codable, Hashable, Sendable { + public var parent: Components.Schemas.Node? { + get { + self.storage.value.parent + } + _modify { + yield &self.storage.value.parent + } + } + public struct childrenPayload: Codable, Hashable, Sendable { + public var additionalProperties: [String: Components.Schemas.Node] + public init(additionalProperties: [String: Components.Schemas.Node] = .init()) { + self.additionalProperties = additionalProperties + } + public init(from decoder: any Swift.Decoder) throws { + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: []) + } + public func encode(to encoder: any Swift.Encoder) throws { + try encoder.encodeAdditionalProperties(additionalProperties) + } + } + public var children: Components.Schemas.Node.childrenPayload? { + get { + self.storage.value.children + } + _modify { + yield &self.storage.value.children + } + } + public init( + parent: Components.Schemas.Node? = nil, + children: Components.Schemas.Node.childrenPayload? = nil + ) { + self.storage = .init(value: .init( + parent: parent, + children: children + )) + } + public enum CodingKeys: String, CodingKey { + case parent + case children + } + public init(from decoder: any Swift.Decoder) throws { + self.storage = try .init(from: decoder) + } + public func encode(to encoder: any Swift.Encoder) throws { + try self.storage.encode(to: encoder) + } + private var storage: OpenAPIRuntime.CopyOnWriteBox + private struct Storage: Codable, Hashable, Sendable { + var parent: Components.Schemas.Node? + struct childrenPayload: Codable, Hashable, Sendable { + public var additionalProperties: [String: Components.Schemas.Node] + public init(additionalProperties: [String: Components.Schemas.Node] = .init()) { + self.additionalProperties = additionalProperties + } + public init(from decoder: any Swift.Decoder) throws { + additionalProperties = try decoder.decodeAdditionalProperties(knownKeys: []) + } + public func encode(to encoder: any Swift.Encoder) throws { + try encoder.encodeAdditionalProperties(additionalProperties) + } + } + var children: Components.Schemas.Node.childrenPayload? + init( + parent: Components.Schemas.Node? = nil, + children: Components.Schemas.Node.childrenPayload? = nil + ) { + self.parent = parent + self.children = children + } + typealias CodingKeys = Components.Schemas.Node.CodingKeys + } + } + } + """ + ) + } + func testComponentsSchemasRecursive_objectNested() throws { try self.assertSchemasTranslation( """