From e5d2a11b25d4196dcd9bafc33575e39932ef256e Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Mon, 5 Oct 2026 16:18:51 +0200 Subject: [PATCH 1/7] Fix SwiftLint warning --- Sources/Encoder/DictionaryComponentEncoder.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/Encoder/DictionaryComponentEncoder.swift b/Sources/Encoder/DictionaryComponentEncoder.swift index a6f4627..4389de7 100644 --- a/Sources/Encoder/DictionaryComponentEncoder.swift +++ b/Sources/Encoder/DictionaryComponentEncoder.swift @@ -250,6 +250,6 @@ extension EncodingError { Use DictionaryNonConformingFloatEncodingStrategy.convertToString to specify how the value should be encoded. """ - return .invalidValue(value, EncodingError.Context(codingPath: codingPath, debugDescription: debugDescription)) + return .invalidValue(value, Context(codingPath: codingPath, debugDescription: debugDescription)) } } From 93bec1a88bde2f856f88504a16fea2990ca7c234 Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Tue, 29 Sep 2026 01:44:42 +0200 Subject: [PATCH 2/7] Fix cursor handling in unkeyed decoding container --- .../DictionaryUnkeyedDecodingContainer.swift | 76 +++++++----- Tests/Decoder/DictionaryDecoderTests.swift | 113 ++++++++++++++++++ 2 files changed, 160 insertions(+), 29 deletions(-) diff --git a/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift b/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift index f7abb78..f7b2d11 100644 --- a/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift @@ -41,7 +41,8 @@ internal final class DictionaryUnkeyedDecodingContainer: // MARK: - Instance Methods - private func popNextComponent() throws -> Any? { + @inline(__always) + private func peekNextComponent() throws -> Any? { guard currentIndex < components.count else { let errorContext = DecodingError.Context( codingPath: currentCodingPath, @@ -51,95 +52,112 @@ internal final class DictionaryUnkeyedDecodingContainer: throw DecodingError.valueNotFound(Any.self, errorContext) } - defer { - currentIndex += 1 - } - return components[currentIndex] } + @inline(__always) + private func decodeNextComponent( + _ decodeComponent: (_ component: Any?, _ codingPath: [CodingKey]) throws -> T + ) throws -> T { + let value = try decodeComponent(try peekNextComponent(), currentCodingPath) + + currentIndex += 1 + + return value + } + + private func superDecoder(for component: Any?, at codingPath: [CodingKey]) -> Decoder { + DictionarySingleValueDecodingContainer( + component: component, + options: options, + userInfo: userInfo, + codingPath: codingPath + ) + } + // MARK: - UnkeyedDecodingContainer internal func decodeNil() throws -> Bool { - decodeNilComponent(from: try popNextComponent()) + guard decodeNilComponent(from: try peekNextComponent()) else { + return false + } + + currentIndex += 1 + + return true } internal func decode(_ type: Bool.Type) throws -> Bool { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int.Type) throws -> Int { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int8.Type) throws -> Int8 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int16.Type) throws -> Int16 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int32.Type) throws -> Int32 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Int64.Type) throws -> Int64 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt.Type) throws -> UInt { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt8.Type) throws -> UInt8 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt16.Type) throws -> UInt16 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt32.Type) throws -> UInt32 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: UInt64.Type) throws -> UInt64 { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Double.Type) throws -> Double { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: Float.Type) throws -> Float { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: String.Type) throws -> String { - try decodeComponentValue(from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(from: $0, at: $1) } } internal func decode(_ type: T.Type) throws -> T { - try decodeComponentValue(of: type, from: try popNextComponent(), at: currentCodingPath) + try decodeNextComponent { try decodeComponentValue(of: type, from: $0, at: $1) } } internal func nestedContainer( keyedBy keyType: NestedKey.Type ) throws -> KeyedDecodingContainer { - try superDecoder().container(keyedBy: keyType) + try decodeNextComponent { try superDecoder(for: $0, at: $1).container(keyedBy: keyType) } } internal func nestedUnkeyedContainer() throws -> UnkeyedDecodingContainer { - try superDecoder().unkeyedContainer() + try decodeNextComponent { try superDecoder(for: $0, at: $1).unkeyedContainer() } } internal func superDecoder() throws -> Decoder { - DictionarySingleValueDecodingContainer( - component: try popNextComponent(), - options: options, - userInfo: userInfo, - codingPath: currentCodingPath - ) + try decodeNextComponent { superDecoder(for: $0, at: $1) } } } diff --git a/Tests/Decoder/DictionaryDecoderTests.swift b/Tests/Decoder/DictionaryDecoderTests.swift index 5228352..df95478 100644 --- a/Tests/Decoder/DictionaryDecoderTests.swift +++ b/Tests/Decoder/DictionaryDecoderTests.swift @@ -333,6 +333,88 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting { assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) } + func testThatDecoderSucceedsWhenDecodingStructUsingDecodeIfPresentOfUnkeyedContainer() { + struct DecodableStruct: Decodable, Equatable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: Int? + let baz: Int? + let qux: Int? + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + bar = try unkeyedContainer.decodeIfPresent(Int.self) + baz = try unkeyedContainer.decodeIfPresent(Int.self) + qux = try unkeyedContainer.decodeIfPresent(Int.self) + } + } + + let dictionary: [String: Any] = ["foo": [123, NSNull(), 456] as [Any]] + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + + func testThatDecoderSucceedsWhenDecodingElementCodingPathsOfUnkeyedContainer() { + struct Element: Decodable, Equatable { + let index: Int? + + init(from decoder: Decoder) throws { + index = decoder.codingPath.last?.intValue + } + } + + struct DecodableStruct: Decodable, Equatable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: Element + let baz: Element + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + bar = try unkeyedContainer.decode(Element.self) + baz = try Element(from: unkeyedContainer.superDecoder()) + } + } + + let dictionary = ["foo": [123, 456]] + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + + func testThatDecoderSucceedsWhenDecodingStructAfterFailedAttemptsInUnkeyedContainer() { + struct DecodableStruct: Decodable, Equatable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: String + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + _ = try? unkeyedContainer.decode(Int.self) + _ = try? unkeyedContainer.decode([Int].self) + _ = try? unkeyedContainer.nestedContainer(keyedBy: CodingKeys.self) + _ = try? unkeyedContainer.nestedUnkeyedContainer() + + bar = try unkeyedContainer.decode(String.self) + } + } + + let dictionary = ["foo": ["bar"]] + + assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) + } + func testThatDecoderSucceedsWhenDecodingStructInSeparateKeyedContainersOfUnkeyedContainer() { struct DecodableStruct: Decodable, Equatable { enum CodingKeys: String, CodingKey { @@ -735,6 +817,37 @@ final class DictionaryDecoderTests: XCTestCase, DictionaryDecoderTesting { } } + func testThatDecoderFailsWhenDecodingInvalidElementForUnkeyedContainer() { + struct DecodableStruct: Decodable { + enum CodingKeys: String, CodingKey { + case foo + } + + let bar: Int + let baz: Int + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + var unkeyedContainer = try container.nestedUnkeyedContainer(forKey: .foo) + + bar = try unkeyedContainer.decode(Int.self) + baz = try unkeyedContainer.decode(Int.self) + } + } + + let dictionary: [String: Any] = ["foo": [123, "456"] as [Any]] + + assertDecoderFails(decoding: DecodableStruct.self, from: dictionary) { error in + switch error { + case let DecodingError.typeMismatch(type, context) where type is Int.Type: + return context.codingPath.map(\.stringValue) == ["foo", "1"] + + default: + return false + } + } + } + // MARK: - XCTestCase override func setUp() { From 79d3b5be7e824f53c7d39430e0e8600ef0fe9590 Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Tue, 29 Sep 2026 02:43:30 +0200 Subject: [PATCH 3/7] Resolve colliding custom keys deterministically --- Sources/Decoder/DictionaryKeyedDecodingContainer.swift | 10 +++++----- .../Options/DictionaryKeyDecodingStrategy.swift | 3 +++ Tests/Decoder/DictionaryDecoderStrategiesTests.swift | 9 +++++++++ 3 files changed, 17 insertions(+), 5 deletions(-) diff --git a/Sources/Decoder/DictionaryKeyedDecodingContainer.swift b/Sources/Decoder/DictionaryKeyedDecodingContainer.swift index 68b89bf..d55086f 100644 --- a/Sources/Decoder/DictionaryKeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryKeyedDecodingContainer.swift @@ -27,12 +27,12 @@ internal class DictionaryKeyedDecodingContainer: case .useDefaultKeys: self.components = components - case let.custom(closure): - let componentKeysAndValues = components.map { key, value in - (closure(codingPath.appending(AnyCodingKey(key))).stringValue, value) - } + case let .custom(closure): + let componentKeysAndValues = components + .sorted { $0.key < $1.key } + .map { key, value in (closure(codingPath.appending(AnyCodingKey(key))).stringValue, value) } - self.components = Dictionary(componentKeysAndValues) { $1 } + self.components = Dictionary(componentKeysAndValues) { first, _ in first } } self.options = options diff --git a/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift index b036eef..aa3870c 100644 --- a/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift @@ -9,5 +9,8 @@ public enum DictionaryKeyDecodingStrategy: Sendable { case useDefaultKeys /// A key decoding strategy defined by the closure you supply. + /// + /// If several keys are converted to the same key, + /// the value of the lexicographically smallest original key is used. case custom(@Sendable (_ codingPath: [CodingKey]) -> CodingKey) } diff --git a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift index fffb9ba..9916dea 100644 --- a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift +++ b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift @@ -48,6 +48,15 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti assertDecoderSucceeds(decoding: DecodableStruct.self, from: dictionary) } + func testThatDecoderSucceedsWhenDecodingCollidingKeysUsingCustomFunctionForKeys() { + decoder.keyDecodingStrategy = .custom { _ in AnyCodingKey("foobar") } + + let letters = "abcdefghijklmnopqrstuvwxyz".map(String.init) + let dictionary = Dictionary(uniqueKeysWithValues: letters.enumerated().map { ($1, $0) }) + + assertDecoderSucceeds(decoding: ["foobar": 0], from: dictionary) + } + // MARK: - func testThatDecoderSucceedsWhenDecodingDate() { From 39c38f076caaba6d7d04edafa80ae083d32b97d6 Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Mon, 5 Oct 2026 16:18:43 +0200 Subject: [PATCH 4/7] Remove unused imports --- Sources/Decoder/DictionaryKeyedDecodingContainer.swift | 2 -- Sources/Decoder/DictionarySingleValueDecodingContainer.swift | 2 -- Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift | 2 -- Sources/Decoder/Options/DictionaryDecodingOptions.swift | 2 -- Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift | 2 -- .../Options/DictionaryNonConformingFloatDecodingStrategy.swift | 2 -- Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift | 2 -- Sources/Encoder/DictionaryComponent.swift | 2 -- Sources/Encoder/DictionaryComponentContainer.swift | 2 -- Sources/Encoder/DictionaryKeyedEncodingContainer.swift | 2 -- Sources/Encoder/DictionarySingleValueEncodingContainer.swift | 2 -- Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift | 2 -- Sources/Encoder/Options/DictionaryEncodingOptions.swift | 2 -- Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift | 2 -- Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift | 2 -- .../Options/DictionaryNonConformingFloatEncodingStrategy.swift | 2 -- Sources/Tools/AnyCodingKey.swift | 2 -- Sources/Tools/Mutex/Mutex.swift | 2 -- Sources/Tools/Mutex/MutexStorage.swift | 2 -- Sources/Tools/Optional+Extensions.swift | 2 -- Sources/Tools/RangeReplaceableCollection+Extensions.swift | 2 -- 21 files changed, 42 deletions(-) diff --git a/Sources/Decoder/DictionaryKeyedDecodingContainer.swift b/Sources/Decoder/DictionaryKeyedDecodingContainer.swift index d55086f..886b4de 100644 --- a/Sources/Decoder/DictionaryKeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryKeyedDecodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal class DictionaryKeyedDecodingContainer: KeyedDecodingContainerProtocol, DictionaryComponentDecoder { diff --git a/Sources/Decoder/DictionarySingleValueDecodingContainer.swift b/Sources/Decoder/DictionarySingleValueDecodingContainer.swift index 6a4f08b..238fe51 100644 --- a/Sources/Decoder/DictionarySingleValueDecodingContainer.swift +++ b/Sources/Decoder/DictionarySingleValueDecodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionarySingleValueDecodingContainer: Decoder, SingleValueDecodingContainer, diff --git a/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift b/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift index f7b2d11..fd70d03 100644 --- a/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift +++ b/Sources/Decoder/DictionaryUnkeyedDecodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryUnkeyedDecodingContainer: UnkeyedDecodingContainer, DictionaryComponentDecoder { diff --git a/Sources/Decoder/Options/DictionaryDecodingOptions.swift b/Sources/Decoder/Options/DictionaryDecodingOptions.swift index fa2a51f..5c8a3d1 100644 --- a/Sources/Decoder/Options/DictionaryDecodingOptions.swift +++ b/Sources/Decoder/Options/DictionaryDecodingOptions.swift @@ -1,5 +1,3 @@ -import Foundation - internal struct DictionaryDecodingOptions { // MARK: - Instance Properties diff --git a/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift index aa3870c..f2e1dc1 100644 --- a/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryKeyDecodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - /// The values that determine how to decode a type’s coding keys from Dictionary keys. public enum DictionaryKeyDecodingStrategy: Sendable { diff --git a/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift index a3dd669..3c2a59c 100644 --- a/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryNonConformingFloatDecodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - /// The strategies for encoding nonconforming floating-point numbers, /// also known as IEEE 754 exceptional values. public enum DictionaryNonConformingFloatDecodingStrategy: Sendable { diff --git a/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift b/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift index 9a4410f..cd99768 100644 --- a/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift +++ b/Sources/Encoder/DictionaryAnyKeyedEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryAnyKeyedEncodingContainer: DictionaryComponentContainer { // MARK: - Instance Properties diff --git a/Sources/Encoder/DictionaryComponent.swift b/Sources/Encoder/DictionaryComponent.swift index 138d310..b1f1a41 100644 --- a/Sources/Encoder/DictionaryComponent.swift +++ b/Sources/Encoder/DictionaryComponent.swift @@ -1,5 +1,3 @@ -import Foundation - internal indirect enum DictionaryComponent { // MARK: - Enumeration Cases diff --git a/Sources/Encoder/DictionaryComponentContainer.swift b/Sources/Encoder/DictionaryComponentContainer.swift index 5fea329..c373861 100644 --- a/Sources/Encoder/DictionaryComponentContainer.swift +++ b/Sources/Encoder/DictionaryComponentContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal protocol DictionaryComponentContainer { // MARK: - Instance Methods diff --git a/Sources/Encoder/DictionaryKeyedEncodingContainer.swift b/Sources/Encoder/DictionaryKeyedEncodingContainer.swift index df841d6..874e3a1 100644 --- a/Sources/Encoder/DictionaryKeyedEncodingContainer.swift +++ b/Sources/Encoder/DictionaryKeyedEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryKeyedEncodingContainer: KeyedEncodingContainerProtocol, DictionaryComponentEncoder { diff --git a/Sources/Encoder/DictionarySingleValueEncodingContainer.swift b/Sources/Encoder/DictionarySingleValueEncodingContainer.swift index d9d6741..99237bc 100644 --- a/Sources/Encoder/DictionarySingleValueEncodingContainer.swift +++ b/Sources/Encoder/DictionarySingleValueEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionarySingleValueEncodingContainer: Encoder, SingleValueEncodingContainer, diff --git a/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift b/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift index 842b89e..34fac91 100644 --- a/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift +++ b/Sources/Encoder/DictionaryUnkeyedEncodingContainer.swift @@ -1,5 +1,3 @@ -import Foundation - internal final class DictionaryUnkeyedEncodingContainer: UnkeyedEncodingContainer, DictionaryComponentContainer, diff --git a/Sources/Encoder/Options/DictionaryEncodingOptions.swift b/Sources/Encoder/Options/DictionaryEncodingOptions.swift index 6878ada..c4dd528 100644 --- a/Sources/Encoder/Options/DictionaryEncodingOptions.swift +++ b/Sources/Encoder/Options/DictionaryEncodingOptions.swift @@ -1,5 +1,3 @@ -import Foundation - internal struct DictionaryEncodingOptions { // MARK: - Instance Properties diff --git a/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift index da26126..deb3215 100644 --- a/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryKeyEncodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - public enum DictionaryKeyEncodingStrategy: Sendable { // MARK: - Enumeration Cases diff --git a/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift index 37abb2a..b321d17 100644 --- a/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryNilEncodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - public enum DictionaryNilEncodingStrategy: Sendable { // MARK: - Enumeration Cases diff --git a/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift index 63cffcd..bff87ba 100644 --- a/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryNonConformingFloatEncodingStrategy.swift @@ -1,5 +1,3 @@ -import Foundation - public enum DictionaryNonConformingFloatEncodingStrategy: Sendable { // MARK: - Enumeration Cases diff --git a/Sources/Tools/AnyCodingKey.swift b/Sources/Tools/AnyCodingKey.swift index b883a4e..1aa1f95 100644 --- a/Sources/Tools/AnyCodingKey.swift +++ b/Sources/Tools/AnyCodingKey.swift @@ -1,5 +1,3 @@ -import Foundation - internal struct AnyCodingKey: CodingKey { // MARK: - Type Properties diff --git a/Sources/Tools/Mutex/Mutex.swift b/Sources/Tools/Mutex/Mutex.swift index 1f3bcb2..9413be2 100644 --- a/Sources/Tools/Mutex/Mutex.swift +++ b/Sources/Tools/Mutex/Mutex.swift @@ -1,5 +1,3 @@ -import Foundation - public struct Mutex: @unchecked Sendable { private let storage: MutexStorage diff --git a/Sources/Tools/Mutex/MutexStorage.swift b/Sources/Tools/Mutex/MutexStorage.swift index a884dad..0602e2d 100644 --- a/Sources/Tools/Mutex/MutexStorage.swift +++ b/Sources/Tools/Mutex/MutexStorage.swift @@ -1,5 +1,3 @@ -import Foundation - import struct os.os_unfair_lock_t import struct os.os_unfair_lock diff --git a/Sources/Tools/Optional+Extensions.swift b/Sources/Tools/Optional+Extensions.swift index 4344e37..2d01ead 100644 --- a/Sources/Tools/Optional+Extensions.swift +++ b/Sources/Tools/Optional+Extensions.swift @@ -1,5 +1,3 @@ -import Foundation - extension Optional { // MARK: - Instance Properties diff --git a/Sources/Tools/RangeReplaceableCollection+Extensions.swift b/Sources/Tools/RangeReplaceableCollection+Extensions.swift index a68dfd6..06c51f1 100644 --- a/Sources/Tools/RangeReplaceableCollection+Extensions.swift +++ b/Sources/Tools/RangeReplaceableCollection+Extensions.swift @@ -1,5 +1,3 @@ -import Foundation - extension RangeReplaceableCollection { // MARK: - Instance Methods From 715595425c3adddf4ab1c97ec0781c2f40db2682 Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Mon, 5 Oct 2026 16:18:51 +0200 Subject: [PATCH 5/7] Remove unused isEmptyOrNil --- Sources/Tools/Optional+Extensions.swift | 9 --------- 1 file changed, 9 deletions(-) diff --git a/Sources/Tools/Optional+Extensions.swift b/Sources/Tools/Optional+Extensions.swift index 2d01ead..141c9ac 100644 --- a/Sources/Tools/Optional+Extensions.swift +++ b/Sources/Tools/Optional+Extensions.swift @@ -6,12 +6,3 @@ extension Optional { self == nil } } - -extension Optional where Wrapped: Collection { - - // MARK: - Instance Properties - - internal var isEmptyOrNil: Bool { - self?.isEmpty ?? true - } -} From e1080dcf4047ab4219c090e53d4f4b79ec50982c Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Mon, 5 Oct 2026 16:19:22 +0200 Subject: [PATCH 6/7] Share the setup of root encoders and decoders --- Sources/Decoder/DictionaryDecoder.swift | 25 +++++++----------- Sources/Encoder/DictionaryEncoder.swift | 34 ++++++++++--------------- 2 files changed, 22 insertions(+), 37 deletions(-) diff --git a/Sources/Decoder/DictionaryDecoder.swift b/Sources/Decoder/DictionaryDecoder.swift index f7e4173..0e86f0c 100644 --- a/Sources/Decoder/DictionaryDecoder.swift +++ b/Sources/Decoder/DictionaryDecoder.swift @@ -54,20 +54,22 @@ public final class DictionaryDecoder: Sendable { // MARK: - Instance Methods - public func decode( - _ type: T.Type = T.self, - from dictionary: [String: Any] - ) throws -> T { + private func rootDecoder(for dictionary: [String: Any]) -> DictionarySingleValueDecodingContainer { let options = optionsMutex.withLock { $0 } - let decoder = DictionarySingleValueDecodingContainer( + return DictionarySingleValueDecodingContainer( component: dictionary, options: options, userInfo: userInfo, codingPath: [] ) + } - return try T(from: decoder) + public func decode( + _ type: T.Type = T.self, + from dictionary: [String: Any] + ) throws -> T { + try T(from: rootDecoder(for: dictionary)) } public func decode(from dictionary: [String: Any]) throws -> T { @@ -80,15 +82,6 @@ public final class DictionaryDecoder: Sendable { from dictionary: [String: Any], configuration: T.DecodingConfiguration ) throws -> T { - let options = optionsMutex.withLock { $0 } - - let decoder = DictionarySingleValueDecodingContainer( - component: dictionary, - options: options, - userInfo: userInfo, - codingPath: [] - ) - - return try T(from: decoder, configuration: configuration) + try T(from: rootDecoder(for: dictionary), configuration: configuration) } } diff --git a/Sources/Encoder/DictionaryEncoder.swift b/Sources/Encoder/DictionaryEncoder.swift index d424b99..3d90275 100644 --- a/Sources/Encoder/DictionaryEncoder.swift +++ b/Sources/Encoder/DictionaryEncoder.swift @@ -61,7 +61,10 @@ public final class DictionaryEncoder: Sendable { // MARK: - Instance Methods - public func encode(_ value: T) throws -> [String: Sendable] { + private func encodeRootValue( + _ value: T, + encoding: (_ encoder: Encoder) throws -> Void + ) throws -> [String: Sendable] { let options = optionsMutex.withLock { $0 } let encoder = DictionarySingleValueEncodingContainer( @@ -70,7 +73,7 @@ public final class DictionaryEncoder: Sendable { codingPath: [] ) - try value.encode(to: encoder) + try encoding(encoder) guard let dictionary = encoder.resolveValue() as? [String: Sendable] else { let errorContext = EncodingError.Context( @@ -84,30 +87,19 @@ public final class DictionaryEncoder: Sendable { return dictionary } + public func encode(_ value: T) throws -> [String: Sendable] { + try encodeRootValue(value) { encoder in + try value.encode(to: encoder) + } + } + @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public func encode( _ value: T, configuration: T.EncodingConfiguration ) throws -> [String: Sendable] { - let options = optionsMutex.withLock { $0 } - - let encoder = DictionarySingleValueEncodingContainer( - options: options, - userInfo: userInfo, - codingPath: [] - ) - - try value.encode(to: encoder, configuration: configuration) - - guard let dictionary = encoder.resolveValue() as? [String: Sendable] else { - let errorContext = EncodingError.Context( - codingPath: [], - debugDescription: "Root component cannot be encoded in Dictionary" - ) - - throw EncodingError.invalidValue(value, errorContext) + try encodeRootValue(value) { encoder in + try value.encode(to: encoder, configuration: configuration) } - - return dictionary } } From 3ef36838fdc72c86f80158a1c4bad41688ccc520 Mon Sep 17 00:00:00 2001 From: Dzmitry Letko Date: Mon, 5 Oct 2026 16:19:40 +0200 Subject: [PATCH 7/7] Remove availability checks below the deployment targets --- Sources/Decoder/DictionaryComponentDecoder.swift | 4 ---- Sources/Decoder/DictionaryDecoder.swift | 1 - Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift | 1 - Sources/Encoder/DictionaryComponentEncoder.swift | 4 ---- Sources/Encoder/DictionaryEncoder.swift | 1 - Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift | 1 - Tests/Decoder/DictionaryDecoderStrategiesTests.swift | 2 -- Tests/Decoder/DictionaryDecoderTesting.swift | 4 ---- Tests/Encoder/DictionaryEncoderStrategiesTests.swift | 2 -- Tests/Encoder/DictionaryEncoderTesting.swift | 4 ---- 10 files changed, 24 deletions(-) diff --git a/Sources/Decoder/DictionaryComponentDecoder.swift b/Sources/Decoder/DictionaryComponentDecoder.swift index be18390..38a9c9c 100644 --- a/Sources/Decoder/DictionaryComponentDecoder.swift +++ b/Sources/Decoder/DictionaryComponentDecoder.swift @@ -103,10 +103,6 @@ extension DictionaryComponentDecoder { return Date(timeIntervalSince1970: try decodePrimitiveValue(from: component, at: codingPath) / 1000.0) case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") - } - let formattedDate = try decodePrimitiveValue(of: String.self, from: component, at: codingPath) guard let date = ISO8601DateFormatter().date(from: formattedDate) else { diff --git a/Sources/Decoder/DictionaryDecoder.swift b/Sources/Decoder/DictionaryDecoder.swift index 0e86f0c..f96ee3d 100644 --- a/Sources/Decoder/DictionaryDecoder.swift +++ b/Sources/Decoder/DictionaryDecoder.swift @@ -76,7 +76,6 @@ public final class DictionaryDecoder: Sendable { try decode(T.self, from: dictionary) } - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public func decode( _ type: T.Type = T.self, from dictionary: [String: Any], diff --git a/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift b/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift index 11b36fd..c0a6caf 100644 --- a/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift +++ b/Sources/Decoder/Options/DictionaryDateDecodingStrategy.swift @@ -15,7 +15,6 @@ public enum DictionaryDateDecodingStrategy: Sendable { case millisecondsSince1970 /// The strategy that formats dates according to the ISO 8601 standard. - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 /// The strategy that defers formatting settings to a supplied date formatter. diff --git a/Sources/Encoder/DictionaryComponentEncoder.swift b/Sources/Encoder/DictionaryComponentEncoder.swift index 4389de7..1d2c655 100644 --- a/Sources/Encoder/DictionaryComponentEncoder.swift +++ b/Sources/Encoder/DictionaryComponentEncoder.swift @@ -72,10 +72,6 @@ extension DictionaryComponentEncoder { return encodePrimitiveValue(date.timeIntervalSince1970, at: codingPath) case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") - } - let formattedDate = ISO8601DateFormatter.string( from: date, timeZone: .iso8601TimeZone, diff --git a/Sources/Encoder/DictionaryEncoder.swift b/Sources/Encoder/DictionaryEncoder.swift index 3d90275..3ece945 100644 --- a/Sources/Encoder/DictionaryEncoder.swift +++ b/Sources/Encoder/DictionaryEncoder.swift @@ -93,7 +93,6 @@ public final class DictionaryEncoder: Sendable { } } - @available(macOS 12, iOS 15, tvOS 15, watchOS 8, *) public func encode( _ value: T, configuration: T.EncodingConfiguration diff --git a/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift b/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift index 92c06cf..f310280 100644 --- a/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift +++ b/Sources/Encoder/Options/DictionaryDateEncodingStrategy.swift @@ -9,7 +9,6 @@ public enum DictionaryDateEncodingStrategy: Sendable { case millisecondsSince1970 case secondsSince1970 - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) case iso8601 case formatted(DateFormatter) diff --git a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift index 9916dea..df9c07c 100644 --- a/Tests/Decoder/DictionaryDecoderStrategiesTests.swift +++ b/Tests/Decoder/DictionaryDecoderStrategiesTests.swift @@ -99,7 +99,6 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti assertDecoderSucceeds(decoding: [String: Date].self, from: dictionary) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) func testThatDecoderSucceedsWhenDecodingDateFromISO8601Format() { decoder.dateDecodingStrategy = .iso8601 @@ -108,7 +107,6 @@ final class DictionaryDecoderStrategiesTests: XCTestCase, DictionaryDecoderTesti assertDecoderSucceeds(decoding: [String: Date].self, from: dictionary) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) func testThatDecoderFailsWhenDecodingInvalidDateFromISO8601Format() { decoder.dateDecodingStrategy = .iso8601 diff --git a/Tests/Decoder/DictionaryDecoderTesting.swift b/Tests/Decoder/DictionaryDecoderTesting.swift index 0e31868..3a68f74 100644 --- a/Tests/Decoder/DictionaryDecoderTesting.swift +++ b/Tests/Decoder/DictionaryDecoderTesting.swift @@ -153,10 +153,6 @@ extension DictionaryDateDecodingStrategy { return .secondsSince1970 case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") - } - return .iso8601 case let .formatted(dateFormatter): diff --git a/Tests/Encoder/DictionaryEncoderStrategiesTests.swift b/Tests/Encoder/DictionaryEncoderStrategiesTests.swift index 4ec139d..5148404 100644 --- a/Tests/Encoder/DictionaryEncoderStrategiesTests.swift +++ b/Tests/Encoder/DictionaryEncoderStrategiesTests.swift @@ -60,7 +60,6 @@ final class DictionaryEncoderStrategiesTests: XCTestCase, DictionaryEncoderTesti assertEncoderSucceeds(encoding: value) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) func testThatEncoderSucceedsWhenEncodingDateToISO8601Format() { encoder.dateEncodingStrategy = .iso8601 @@ -69,7 +68,6 @@ final class DictionaryEncoderStrategiesTests: XCTestCase, DictionaryEncoderTesti assertEncoderSucceeds(encoding: value) } - @available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) func testThatEncoderSucceedsWhenEncodingDateUsingFormatter() { let dateFormatter = DateFormatter() diff --git a/Tests/Encoder/DictionaryEncoderTesting.swift b/Tests/Encoder/DictionaryEncoderTesting.swift index ba17b03..28c2345 100644 --- a/Tests/Encoder/DictionaryEncoderTesting.swift +++ b/Tests/Encoder/DictionaryEncoderTesting.swift @@ -119,10 +119,6 @@ extension DictionaryDateEncodingStrategy { return .secondsSince1970 case .iso8601: - guard #available(macOS 10.12, iOS 10.0, watchOS 3.0, tvOS 10.0, *) else { - fatalError("ISO8601DateFormatter is unavailable on this platform.") - } - return .iso8601 case let .formatted(dateFormatter):