Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
e5d2a11
Fix SwiftLint warning
letko-dmitry Oct 5, 2026
93bec1a
Fix cursor handling in unkeyed decoding container
letko-dmitry Sep 28, 2026
79d3b5b
Resolve colliding custom keys deterministically
letko-dmitry Sep 29, 2026
39c38f0
Remove unused imports
letko-dmitry Oct 5, 2026
7155954
Remove unused isEmptyOrNil
letko-dmitry Oct 5, 2026
e1080dc
Share the setup of root encoders and decoders
letko-dmitry Oct 5, 2026
3ef3683
Remove availability checks below the deployment targets
letko-dmitry Oct 5, 2026
e8db020
Decode numbers of other types like NSNumber
letko-dmitry Oct 5, 2026
04e3a83
Compare the types of values instead of casting them
letko-dmitry Oct 5, 2026
be34d14
Add decimal strategies that keep Decimal as a number
letko-dmitry Oct 5, 2026
edd17ba
Support Int128 and UInt128
letko-dmitry Oct 5, 2026
653b284
Add ISO 8601 styles and format dates with a shared format style
letko-dmitry Oct 5, 2026
cf01d57
Inline hot paths and consume stored arguments
letko-dmitry Sep 29, 2026
50a72b1
Remove redundant allocations and linear key lookup
letko-dmitry Sep 29, 2026
bf11de2
Make keyed decoding container final
letko-dmitry Sep 29, 2026
e51f649
Check for NSNull by type
letko-dmitry Sep 29, 2026
b89e603
Check the non-conforming float strategy before casting to strings
letko-dmitry Sep 29, 2026
9b4118e
Reserve room for two elements in unkeyed containers
letko-dmitry Sep 29, 2026
6917e1a
Look keys up once when decoding optional values
letko-dmitry Sep 29, 2026
960990e
Bridge Foundation numbers, strings and data directly
letko-dmitry Oct 5, 2026
801aa0d
Build coding paths lazily
letko-dmitry Sep 29, 2026
ab9f642
Read Foundation arrays in place
letko-dmitry Sep 29, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 30 additions & 10 deletions DictionaryCoder.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

375 changes: 298 additions & 77 deletions Sources/Decoder/DictionaryComponentDecoder.swift

Large diffs are not rendered by default.

33 changes: 16 additions & 17 deletions Sources/Decoder/DictionaryDecoder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,11 @@ public final class DictionaryDecoder: Sendable {
set { optionsMutex.withLock { $0.dataDecodingStrategy = newValue } }
}

public var decimalDecodingStrategy: DictionaryDecimalDecodingStrategy {
get { optionsMutex.withLock { $0.decimalDecodingStrategy } }
set { optionsMutex.withLock { $0.decimalDecodingStrategy = newValue } }
}

public var nonConformingFloatDecodingStrategy: DictionaryNonConformingFloatDecodingStrategy {
get { optionsMutex.withLock { $0.nonConformingFloatDecodingStrategy } }
set { optionsMutex.withLock { $0.nonConformingFloatDecodingStrategy = newValue } }
Expand All @@ -37,13 +42,15 @@ public final class DictionaryDecoder: Sendable {
public init(
dateDecodingStrategy: DictionaryDateDecodingStrategy = .deferredToDate,
dataDecodingStrategy: DictionaryDataDecodingStrategy = .base64,
decimalDecodingStrategy: DictionaryDecimalDecodingStrategy = .deferredToDecimal,
nonConformingFloatDecodingStrategy: DictionaryNonConformingFloatDecodingStrategy = .throw,
keyDecodingStrategy: DictionaryKeyDecodingStrategy = .useDefaultKeys,
userInfo: [CodingUserInfoKey: Sendable] = [:]
) {
let options = DictionaryDecodingOptions(
dateDecodingStrategy: dateDecodingStrategy,
dataDecodingStrategy: dataDecodingStrategy,
decimalDecodingStrategy: decimalDecodingStrategy,
nonConformingFloatDecodingStrategy: nonConformingFloatDecodingStrategy,
keyDecodingStrategy: keyDecodingStrategy
)
Expand All @@ -54,41 +61,33 @@ public final class DictionaryDecoder: Sendable {

// MARK: - Instance Methods

public func decode<T: Decodable>(
_ 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<T: Decodable>(
_ type: T.Type = T.self,
from dictionary: [String: Any]
) throws -> T {
try T(from: rootDecoder(for: dictionary))
}

public func decode<T: Decodable>(from dictionary: [String: Any]) throws -> T {
try decode(T.self, from: dictionary)
}

@available(macOS 12, iOS 15, tvOS 15, watchOS 8, *)
public func decode<T: DecodableWithConfiguration>(
_ type: T.Type = T.self,
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)
}
}
151 changes: 142 additions & 9 deletions Sources/Decoder/DictionaryKeyedDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import Foundation

internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
internal final class DictionaryKeyedDecodingContainer<Key: CodingKey>:
KeyedDecodingContainerProtocol,
DictionaryComponentDecoder {

Expand All @@ -27,12 +25,12 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
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
Expand All @@ -42,6 +40,7 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:

// MARK: - Instance Methods

@inline(__always)
private func component<T>(of type: T.Type = T.self, forKey key: Key) throws -> T {
let anyComponent = components[key.stringValue]

Expand All @@ -57,6 +56,18 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
return component
}

// Unlike the default implementation of `decodeIfPresent`, looks the key up once rather than in `contains`,
// `decodeNil` and `decode`.
@inline(__always)
private func decodeIfPresent<T>(forKey key: Key, _ decode: (_ component: Any) throws -> T) rethrows -> T? {
guard let component = components[key.stringValue], !decodeNilComponent(from: component) else {
return nil
}

return try decode(component)
}

@inline(__always)
private func superDecoder(forAnyKey key: CodingKey) throws -> Decoder {
DictionarySingleValueDecodingContainer(
component: components[key.stringValue],
Expand All @@ -69,7 +80,7 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
// MARK: - KeyedDecodingContainerProtocol

internal func contains(_ key: Key) -> Bool {
components.contains { $0.key == key.stringValue }
components.keys.contains(key.stringValue)
}

internal func decodeNil(forKey key: Key) throws -> Bool {
Expand Down Expand Up @@ -100,6 +111,13 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: Int128.Type, forKey key: Key) throws -> Int128 {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
#endif

internal func decode(_ type: UInt.Type, forKey key: Key) throws -> UInt {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
Expand All @@ -120,6 +138,13 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: UInt128.Type, forKey key: Key) throws -> UInt128 {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
#endif

internal func decode(_ type: Double.Type, forKey key: Key) throws -> Double {
try decodeComponentValue(from: try component(forKey: key), at: codingPath.appending(key))
}
Expand All @@ -136,6 +161,114 @@ internal class DictionaryKeyedDecodingContainer<Key: CodingKey>:
try decodeComponentValue(of: type, from: try component(forKey: key), at: codingPath.appending(key))
}

internal func decodeIfPresent(_ type: Bool.Type, forKey key: Key) throws -> Bool? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: Int.Type, forKey key: Key) throws -> Int? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: Int8.Type, forKey key: Key) throws -> Int8? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: Int16.Type, forKey key: Key) throws -> Int16? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: Int32.Type, forKey key: Key) throws -> Int32? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: Int64.Type, forKey key: Key) throws -> Int64? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decodeIfPresent(_ type: Int128.Type, forKey key: Key) throws -> Int128? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}
#endif

internal func decodeIfPresent(_ type: UInt.Type, forKey key: Key) throws -> UInt? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: UInt8.Type, forKey key: Key) throws -> UInt8? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: UInt16.Type, forKey key: Key) throws -> UInt16? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: UInt32.Type, forKey key: Key) throws -> UInt32? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: UInt64.Type, forKey key: Key) throws -> UInt64? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decodeIfPresent(_ type: UInt128.Type, forKey key: Key) throws -> UInt128? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}
#endif

internal func decodeIfPresent(_ type: Double.Type, forKey key: Key) throws -> Double? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: Float.Type, forKey key: Key) throws -> Float? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent(_ type: String.Type, forKey key: Key) throws -> String? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(from: component, at: codingPath.appending(key))
}
}

internal func decodeIfPresent<T: Decodable>(_ type: T.Type, forKey key: Key) throws -> T? {
try decodeIfPresent(forKey: key) { component in
try decodeComponentValue(of: type, from: component, at: codingPath.appending(key))
}
}

internal func nestedContainer<NestedKey: CodingKey>(
keyedBy keyType: NestedKey.Type,
forKey key: Key
Expand Down
18 changes: 15 additions & 3 deletions Sources/Decoder/DictionarySingleValueDecodingContainer.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import Foundation

internal final class DictionarySingleValueDecodingContainer:
Decoder,
SingleValueDecodingContainer,
Expand Down Expand Up @@ -56,6 +54,13 @@ internal final class DictionarySingleValueDecodingContainer:
try decodeComponentValue(from: component, at: codingPath)
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: Int128.Type) throws -> Int128 {
try decodeComponentValue(from: component, at: codingPath)
}
#endif

internal func decode(_ type: UInt.Type) throws -> UInt {
try decodeComponentValue(from: component, at: codingPath)
}
Expand All @@ -76,6 +81,13 @@ internal final class DictionarySingleValueDecodingContainer:
try decodeComponentValue(from: component, at: codingPath)
}

#if compiler(>=6.0)
@available(macOS 15.0, iOS 18.0, watchOS 11.0, tvOS 18.0, visionOS 2.0, *)
internal func decode(_ type: UInt128.Type) throws -> UInt128 {
try decodeComponentValue(from: component, at: codingPath)
}
#endif

internal func decode(_ type: Double.Type) throws -> Double {
try decodeComponentValue(from: component, at: codingPath)
}
Expand Down Expand Up @@ -110,7 +122,7 @@ internal final class DictionarySingleValueDecodingContainer:
}

internal func unkeyedContainer() throws -> UnkeyedDecodingContainer {
guard let components = component as? [Any?] else {
guard let components = DictionaryUnkeyedComponents(component) else {
throw DecodingError.unkeyedContainerTypeMismatch(at: codingPath, component: component)
}

Expand Down
Loading
Loading