Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,37 +13,35 @@ extension KeyedDecodingContainer {
_ type: DefaultEmptyPolymorphicArrayValue<T>.Type,
forKey key: Key
) throws -> DefaultEmptyPolymorphicArrayValue<T> where T: PolymorphicCodableStrategy {
// Check if key exists
guard contains(key) else {
switch try polymorphicKeyPresence(forKey: key) {
case .missing:
return DefaultEmptyPolymorphicArrayValue(wrappedValue: [], outcome: .keyNotFound)
}

// Check if value is null
if try decodeNil(forKey: key) {
case .null:
return DefaultEmptyPolymorphicArrayValue(wrappedValue: [], outcome: .valueWasNil)
}

// Try to decode using the property wrapper's decoder
let decoder = try superDecoder(forKey: key)
return try DefaultEmptyPolymorphicArrayValue(from: decoder)
case .present:
// Try to decode using the property wrapper's decoder
let decoder = try superDecoder(forKey: key)
return try DefaultEmptyPolymorphicArrayValue(from: decoder)
}
}

public func decodeIfPresent<T>(
_ type: DefaultEmptyPolymorphicArrayValue<T>.Type,
forKey key: Self.Key
) throws -> DefaultEmptyPolymorphicArrayValue<T>? where T: PolymorphicCodableStrategy {
// Check if key exists
guard contains(key) else {
switch try polymorphicKeyPresence(forKey: key) {
case .missing:
return nil
}

// Check if value is null
if try decodeNil(forKey: key) {
case .null:
return DefaultEmptyPolymorphicArrayValue(wrappedValue: [], outcome: .valueWasNil)
}

// Try to decode using the property wrapper's decoder
let decoder = try superDecoder(forKey: key)
return try DefaultEmptyPolymorphicArrayValue(from: decoder)
case .present:
// Try to decode using the property wrapper's decoder
let decoder = try superDecoder(forKey: key)
return try DefaultEmptyPolymorphicArrayValue(from: decoder)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,30 +24,29 @@ extension KeyedDecodingContainer {
_ type: LossyOptionalPolymorphicValue<T>.Type,
forKey key: Self.Key
) throws -> LossyOptionalPolymorphicValue<T>? where T: PolymorphicCodableStrategy {
// Check if key exists
guard contains(key) else {
switch try polymorphicKeyPresence(forKey: key) {
case .missing:
return nil
}

// Check if value is null
if try decodeNil(forKey: key) {
case .null:
return LossyOptionalPolymorphicValue(wrappedValue: nil, outcome: .valueWasNil)
}

// Try to decode the polymorphic value
do {
let decoder = try superDecoder(forKey: key)
let value = try T.decode(from: decoder)
return LossyOptionalPolymorphicValue(wrappedValue: value, outcome: .decodedSuccessfully)
} catch {
#if DEBUG
/// Report error to resilient decoding error reporter
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return LossyOptionalPolymorphicValue(wrappedValue: nil, outcome: .recoveredFrom(error, wasReported: true))
#else
return LossyOptionalPolymorphicValue(wrappedValue: nil, outcome: .recoveredFrom(error, wasReported: false))
#endif
case .present:
// Try to decode the polymorphic value
do {
let decoder = try superDecoder(forKey: key)
let value = try T.decode(from: decoder)
return LossyOptionalPolymorphicValue(wrappedValue: value, outcome: .decodedSuccessfully)
} catch {
#if DEBUG
/// Report error to resilient decoding error reporter
let decoder = try? superDecoder(forKey: key)
decoder?.reportError(error)
return LossyOptionalPolymorphicValue(wrappedValue: nil, outcome: .recoveredFrom(error, wasReported: true))
#else
return LossyOptionalPolymorphicValue(wrappedValue: nil, outcome: .recoveredFrom(error, wasReported: false))
#endif
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,35 +24,34 @@ extension KeyedDecodingContainer {
_ type: OptionalPolymorphicArrayValue<T>.Type,
forKey key: Self.Key
) throws -> OptionalPolymorphicArrayValue<T>? where T: PolymorphicCodableStrategy {
// Check if the key exists
guard contains(key) else {
switch try polymorphicKeyPresence(forKey: key) {
case .missing:
return nil
}

// Check if the value is null
if try decodeNil(forKey: key) {
case .null:
return OptionalPolymorphicArrayValue(wrappedValue: nil, outcome: .valueWasNil)
}

// Try to decode the array
do {
var container = try nestedUnkeyedContainer(forKey: key)
var elements = [T.ExpectedType]()
case .present:
// Try to decode the array
do {
var container = try nestedUnkeyedContainer(forKey: key)
var elements = [T.ExpectedType]()

while !container.isAtEnd {
// Use PolymorphicValue for decoding each element
let value = try container.decode(PolymorphicValue<T>.self)
elements.append(value.wrappedValue)
}
while !container.isAtEnd {
// Use PolymorphicValue for decoding each element
let value = try container.decode(PolymorphicValue<T>.self)
elements.append(value.wrappedValue)
}

return OptionalPolymorphicArrayValue(wrappedValue: elements, outcome: .decodedSuccessfully)
} catch {
#if DEBUG
// Report the error through superDecoder
let decoder = try superDecoder(forKey: key)
decoder.reportError(error)
#endif
throw error
return OptionalPolymorphicArrayValue(wrappedValue: elements, outcome: .decodedSuccessfully)
} catch {
#if DEBUG
// Report the error through superDecoder
let decoder = try superDecoder(forKey: key)
decoder.reportError(error)
#endif
throw error
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,24 +24,17 @@ extension KeyedDecodingContainer {
_ type: OptionalPolymorphicValue<T>.Type,
forKey key: Self.Key
) throws -> OptionalPolymorphicValue<T>? where T: PolymorphicCodableStrategy {
// Check if key exists
guard contains(key) else {
switch try polymorphicKeyPresence(forKey: key) {
case .missing:
return nil
}

// Check if value is null
if try decodeNil(forKey: key) {
case .null:
return OptionalPolymorphicValue(wrappedValue: nil, outcome: .valueWasNil)
}

// Try to decode the polymorphic value
do {
let decoder = try superDecoder(forKey: key)
let value = try T.decode(from: decoder)
return OptionalPolymorphicValue(wrappedValue: value, outcome: .decodedSuccessfully)
} catch {
case .present:
// OptionalPolymorphicValue throws errors instead of recovering
throw error
let value = try T.decode(from: superDecoder(forKey: key))
return OptionalPolymorphicValue(wrappedValue: value, outcome: .decodedSuccessfully)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
//
// KeyedDecodingContainer+PolymorphicKeyPresence.swift
// KarrotCodableKit
//
// Copyright © 2025 Danggeun Market Inc. All rights reserved.
//

import Foundation

/// Three-way result of probing a key before polymorphic decoding.
enum PolymorphicKeyPresence {
/// The key is absent from the container.
case missing
/// The key is present but its value is JSON `null`.
case null
/// The key is present with a non-null value.
case present
}

extension KeyedDecodingContainer {
/// Resolves whether a key is missing, null, or present.
///
/// Centralizes the `contains` / `decodeNil` ordering that every polymorphic property-wrapper
/// decode overload previously duplicated. The caller acquires its own `superDecoder(forKey:)` or
/// `nestedUnkeyedContainer(forKey:)` in the `.present` case, matching each wrapper's existing path.
func polymorphicKeyPresence(forKey key: Key) throws -> PolymorphicKeyPresence {
guard contains(key) else { return .missing }
return try decodeNil(forKey: key) ? .null : .present
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ extension KeyedDecodingContainer {
_: PolymorphicLossyArrayValue<T>.Type,
forKey key: Key
) throws -> PolymorphicLossyArrayValue<T> where T: PolymorphicCodableStrategy {
// Return empty array if key is missing
guard contains(key) else {
switch try polymorphicKeyPresence(forKey: key) {
case .missing:
// Return empty array if key is missing
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
Expand All @@ -31,10 +32,8 @@ extension KeyedDecodingContainer {
#else
return PolymorphicLossyArrayValue(wrappedValue: [], outcome: .keyNotFound)
#endif
}

// Check if value is null
if try decodeNil(forKey: key) {
case .null:
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
Expand All @@ -51,37 +50,36 @@ extension KeyedDecodingContainer {
#else
return PolymorphicLossyArrayValue(wrappedValue: [], outcome: .valueWasNil)
#endif
}

// Try to decode the array
do {
let decoder = try superDecoder(forKey: key)
return try PolymorphicLossyArrayValue(from: decoder)
} catch {
// If decoding fails (e.g., not an array), return empty array
#if DEBUG
return PolymorphicLossyArrayValue(
wrappedValue: [],
outcome: .recoveredFrom(error, wasReported: false),
results: []
)
#else
return PolymorphicLossyArrayValue(wrappedValue: [], outcome: .recoveredFrom(error, wasReported: false))
#endif
case .present:
// Try to decode the array
do {
let decoder = try superDecoder(forKey: key)
return try PolymorphicLossyArrayValue(from: decoder)
} catch {
// If decoding fails (e.g., not an array), return empty array
#if DEBUG
return PolymorphicLossyArrayValue(
wrappedValue: [],
outcome: .recoveredFrom(error, wasReported: false),
results: []
)
#else
return PolymorphicLossyArrayValue(wrappedValue: [], outcome: .recoveredFrom(error, wasReported: false))
#endif
}
}
}

public func decodeIfPresent<T>(
_: PolymorphicLossyArrayValue<T>.Type,
forKey key: Self.Key
) throws -> PolymorphicLossyArrayValue<T>? where T: PolymorphicCodableStrategy {
// Check if key exists
guard contains(key) else {
switch try polymorphicKeyPresence(forKey: key) {
case .missing:
return nil
}

// Check if value is null
if try decodeNil(forKey: key) {
case .null:
#if DEBUG
let context = DecodingError.Context(
codingPath: codingPath + [key],
Expand All @@ -96,12 +94,14 @@ extension KeyedDecodingContainer {
results: []
)
#else
return PolymorphicLossyArrayValue(wrappedValue: [])
// Match `decode(_:forKey:)`: a null value maps to `.valueWasNil`, not `.decodedSuccessfully`.
return PolymorphicLossyArrayValue(wrappedValue: [], outcome: .valueWasNil)
#endif
}

// Try to decode using PolymorphicLossyArrayValue's decoder
let decoder = try superDecoder(forKey: key)
return try PolymorphicLossyArrayValue(from: decoder)
case .present:
// Try to decode using PolymorphicLossyArrayValue's decoder
let decoder = try superDecoder(forKey: key)
return try PolymorphicLossyArrayValue(from: decoder)
}
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
//
// KeyedEncodingContainer+OmitNilPolymorphicEncodable.swift
// KarrotCodableKit
//
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

/// An optional polymorphic property wrapper whose key should be omitted when its value is `nil`.
public protocol OmitNilPolymorphicEncodable: Encodable {
/// `true` when the wrapped optional value is `nil`.
var isWrappedValueNil: Bool { get }
}

extension KeyedEncodingContainer {
/// Encodes an omit-nil polymorphic wrapper, skipping the key entirely when its value is `nil`.
///
/// This mirrors Apple's default `Codable` behavior for optional properties, where a `nil` value
/// results in the key being skipped rather than encoded as an explicit `null`. It is the encoding-side
/// counterpart to the `decode(_:forKey:)` overload that treats a missing key as `nil`.
///
/// This overload is chosen by overload resolution over the generic `encode<T: Encodable>` because
/// its `OmitNilPolymorphicEncodable` constraint is more specific.
public mutating func encode<Wrapper>(
_ value: Wrapper,
forKey key: Key
) throws where Wrapper: OmitNilPolymorphicEncodable {
guard !value.isWrappedValueNil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}

extension OptionalPolymorphicValue: OmitNilPolymorphicEncodable {
public var isWrappedValueNil: Bool { wrappedValue == nil }
}

extension LossyOptionalPolymorphicValue: OmitNilPolymorphicEncodable {
public var isWrappedValueNil: Bool { wrappedValue == nil }
}

extension OptionalPolymorphicArrayValue: OmitNilPolymorphicEncodable {
public var isWrappedValueNil: Bool { wrappedValue == nil }
}

extension OptionalPolymorphicLossyArrayValue: OmitNilPolymorphicEncodable {
public var isWrappedValueNil: Bool { wrappedValue == nil }
}
Loading