Skip to content
Merged
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
6a06c7e
docs: Add ResilientDecoding license and update README
ElonPark Jul 28, 2025
3dc5264
docs: Update README.md with ResilientDecoding information
ElonPark Jul 29, 2025
efd36da
feat: Add Resilient Decoding core infrastructure
ElonPark Jul 29, 2025
19234f0
feat: Add BetterCodable projected value infrastructure
ElonPark Jul 29, 2025
ddd2dbe
feat: Add PolymorphicCodable projected value infrastructure
ElonPark Jul 29, 2025
f18b396
test: Reorganize PolymorphicCodable test files
ElonPark Jul 29, 2025
3d06adb
test: Replace force unwrap with #require in UnnestedPolymorphicDecoda…
ElonPark Jul 29, 2025
d5ee956
feat: Add resilient decoding support to DefaultCodable
ElonPark Jul 29, 2025
bb08f3d
feat: Add resilient decoding support to DataValue
ElonPark Jul 29, 2025
ce476b9
feat: Add resilient decoding support to DateValue
ElonPark Jul 29, 2025
fcbcfb0
feat: Add resilient decoding support to LosslessValue
ElonPark Jul 29, 2025
f26288d
feat: Add resilient decoding support to LossyValue
ElonPark Jul 29, 2025
6ae0db1
feat: Add resilient decoding support to PolymorphicValue
ElonPark Jul 29, 2025
74a872a
feat: Add resilient decoding support to OptionalPolymorphicValue
ElonPark Jul 29, 2025
4b5283c
feat: Add resilient decoding support to LossyOptionalPolymorphicValue
ElonPark Jul 29, 2025
b3ef2a6
feat: Add resilient decoding support to OptionalPolymorphicArrayValue
ElonPark Jul 29, 2025
5c7da50
feat: Add resilient decoding support to DefaultEmptyPolymorphicArrayV…
ElonPark Jul 29, 2025
91cbf76
feat: Add resilient decoding support to PolymorphicLossyArrayValue
ElonPark Jul 29, 2025
c31eebe
fix: Update resilient decoding tests to work with changed ErrorReport…
ElonPark Jul 29, 2025
3449843
test: apply coderabbit comments
ElonPark Jul 31, 2025
74f3bbc
docs: update README.md, PolymorphicCodableStrategyProviding comments
ElonPark Aug 5, 2025
8884c8b
ci: xcode version upgrade
ElonPark Aug 5, 2025
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ jobs:
runs-on: macos-15
strategy:
matrix:
xcode: ['16.1']
xcode: ['16.4']
config: ['debug', 'release']

steps:
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,10 +120,10 @@ extension User: Codable {

### PolymorphicCodable

`PolymorphicCodable` provides functionality to easily decode polymorphic types from JSON. It includes several interfaces like `PolymorphicIdentifiable`, `PolymorphicCodableStrategy`, and property wrappers like `PolymorphicValue` and `PolymorphicArrayValue`.
`PolymorphicCodable` provides functionality to easily decode polymorphic types from JSON. This functionality provides Swift implementation for the OpenAPI Specification's `oneOf` pattern, allowing type-safe handling of multiple possible schemas. It includes several interfaces like `PolymorphicIdentifiable`, `PolymorphicCodableStrategy`, and property wrappers like `PolymorphicValue` and `PolymorphicArrayValue`.

**Parameters:**
- `identifierCodingKey`: Specifies the JSON key used to determine the type of object being decoded. Defaults to "type" if not specified, allowing you to omit this parameter when using the default value.
- `identifierCodingKey`: Specifies the JSON key used to determine the type of object being decoded. This parameter corresponds to the `discriminator.propertyName` in OpenAPI Specification's oneOf definition. Defaults to "type" if not specified, allowing you to omit this parameter when using the default value.
- `fallbackType`: Defines a default type to use when the identifier in the JSON doesn't match any of the registered types, preventing decoding failures for unknown types. If this parameter is omitted and an unknown type identifier is encountered during decoding, a decoding error will be thrown.

The following example demonstrates how to decode dynamic JSON content where the type of object is determined at runtime:
Expand Down Expand Up @@ -277,3 +277,4 @@ This project is licensed under the MIT. See LICENSE for details.
- PolymorphicCodable was inspired by [Encode and decode polymorphic types in Swift](https://nilcoalescing.com/blog/BringingPolymorphismToCodable/).
- AnyCodable was adapted from [Flight-School/AnyCodable](https://github.com/Flight-School/AnyCodable).
- BetterCodable was adapted from [marksands/BetterCodable](https://github.com/marksands/BetterCodable).
- ResilientDecodingErrorReporter was adapted from [airbnb/ResilientDecoding](https://github.com/airbnb/ResilientDecoding).
35 changes: 32 additions & 3 deletions Sources/KarrotCodableKit/BetterCodable/DataValue/DataValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,33 @@ public protocol DataValueCodableStrategy {
public struct DataValue<Coder: DataValueCodableStrategy> {
public var wrappedValue: Coder.DataType

public let outcome: ResilientDecodingOutcome

public init(wrappedValue: Coder.DataType) {
self.wrappedValue = wrappedValue
self.outcome = .decodedSuccessfully
}

init(wrappedValue: Coder.DataType, outcome: ResilientDecodingOutcome) {
self.wrappedValue = wrappedValue
self.outcome = outcome
}

#if DEBUG
public var projectedValue: ResilientProjectedValue { ResilientProjectedValue(outcome: outcome) }
#endif
}

extension DataValue: Decodable {
public init(from decoder: Decoder) throws {
self.wrappedValue = try Coder.decode(String(from: decoder))
do {
let stringValue = try String(from: decoder)
self.wrappedValue = try Coder.decode(stringValue)
self.outcome = .decodedSuccessfully
} catch {
decoder.reportError(error)
throw error
}
}
}

Expand All @@ -42,6 +61,16 @@ extension DataValue: Encodable {
}
}

extension DataValue: Equatable where Coder.DataType: Equatable {}
extension DataValue: Hashable where Coder.DataType: Hashable {}
extension DataValue: Equatable where Coder.DataType: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
}

extension DataValue: Hashable where Coder.DataType: Hashable {
public func hash(into hasher: inout Hasher) {
hasher.combine(wrappedValue)
}
}

extension DataValue: Sendable where Coder.DataType: Sendable {}
24 changes: 21 additions & 3 deletions Sources/KarrotCodableKit/BetterCodable/DateValue/DateValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,33 @@ public protocol DateValueCodableStrategy {
public struct DateValue<Formatter: DateValueCodableStrategy> {
public var wrappedValue: Date

public let outcome: ResilientDecodingOutcome

public init(wrappedValue: Date) {
self.wrappedValue = wrappedValue
self.outcome = .decodedSuccessfully
}

init(wrappedValue: Date, outcome: ResilientDecodingOutcome) {
self.wrappedValue = wrappedValue
self.outcome = outcome
}

#if DEBUG
public var projectedValue: ResilientProjectedValue { ResilientProjectedValue(outcome: outcome) }
#endif
}

extension DateValue: Decodable where Formatter.RawValue: Decodable {
public init(from decoder: Decoder) throws {
let value = try Formatter.RawValue(from: decoder)
self.wrappedValue = try Formatter.decode(value)
do {
let value = try Formatter.RawValue(from: decoder)
self.wrappedValue = try Formatter.decode(value)
self.outcome = .decodedSuccessfully
} catch {
decoder.reportError(error)
throw error
}
}
}

Expand All @@ -45,7 +63,7 @@ extension DateValue: Encodable where Formatter.RawValue: Encodable {
}

extension DateValue: Equatable {
public static func == (lhs: DateValue<Formatter>, rhs: DateValue<Formatter>) -> Bool {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,19 +26,39 @@ public protocol OptionalDateValueCodableStrategy {
public struct OptionalDateValue<Formatter: OptionalDateValueCodableStrategy> {
public var wrappedValue: Date?

public let outcome: ResilientDecodingOutcome

public init(wrappedValue: Date?) {
self.wrappedValue = wrappedValue
self.outcome = .decodedSuccessfully
}

init(wrappedValue: Date?, outcome: ResilientDecodingOutcome) {
self.wrappedValue = wrappedValue
self.outcome = outcome
}

#if DEBUG
public var projectedValue: ResilientProjectedValue { ResilientProjectedValue(outcome: outcome) }
#endif
}

extension OptionalDateValue: Decodable where Formatter.RawValue: Decodable {
public init(from decoder: Decoder) throws {
do {
let value = try Formatter.RawValue(from: decoder)
self.wrappedValue = try Formatter.decode(value)
do {
self.wrappedValue = try Formatter.decode(value)
self.outcome = .decodedSuccessfully
} catch {
decoder.reportError(error)
throw error
}
} catch DecodingError.valueNotFound(let rawType, _) where rawType == Formatter.RawValue.self {
self.wrappedValue = nil
self.outcome = .valueWasNil
} catch {
decoder.reportError(error)
throw error
}
}
Expand All @@ -52,7 +72,7 @@ extension OptionalDateValue: Encodable where Formatter.RawValue: Encodable {
}

extension OptionalDateValue: Equatable {
public static func == (lhs: OptionalDateValue<Formatter>, rhs: OptionalDateValue<Formatter>) -> Bool {
public static func == (lhs: Self, rhs: Self) -> Bool {
lhs.wrappedValue == rhs.wrappedValue
}
}
Expand Down
Loading
Loading