Skip to content
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// KeyedEncodingContainer+OptionalDateValue.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

extension KeyedEncodingContainer {
/// Encodes an `OptionalDateValue`, omitting the key entirely when the wrapped 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`.
public mutating func encode<T>(
_ value: OptionalDateValue<T>,
forKey key: Key
) throws where T.RawValue: Encodable {
guard value.wrappedValue != nil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// KeyedEncodingContainer+OptionalLosslessValue.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

extension KeyedEncodingContainer {
/// Encodes an `OptionalLosslessValueCodable`, omitting the key entirely when the wrapped 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`.
public mutating func encode<T>(
_ value: OptionalLosslessValueCodable<T>,
forKey key: Key
) throws where T: LosslessDecodingStrategy {
guard value.wrappedValue != nil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
//
// KeyedEncodingContainer+LossyOptional.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

extension KeyedEncodingContainer {
/// Encodes a `@LossyOptional` value, omitting the key entirely when the wrapped value is `nil`.
///
/// `LossyOptional` is a `DefaultCodable<DefaultNilStrategy>` alias, so this overload is constrained to
/// `DefaultNilStrategy` to target only the optional case. Other `DefaultCodable` wrappers
/// (`@DefaultFalse`, `@DefaultEmptyArray`, ...) keep encoding their non-optional default value.
///
/// 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 the default `nil`.
public mutating func encode<T>(
_ value: DefaultCodable<DefaultNilStrategy<T>>,
forKey key: Key
) throws where T: Encodable {
guard value.wrappedValue != nil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// KeyedEncodingContainer+LossyOptionalPolymorphicValue.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

extension KeyedEncodingContainer {
/// Encodes a `LossyOptionalPolymorphicValue`, omitting the key entirely when the wrapped 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`.
public mutating func encode<T>(
_ value: LossyOptionalPolymorphicValue<T>,
forKey key: Key
) throws where T: PolymorphicCodableStrategy {
guard value.wrappedValue != nil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// KeyedEncodingContainer+OptionalPolymorphicArrayValue.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

extension KeyedEncodingContainer {
/// Encodes an `OptionalPolymorphicArrayValue`, omitting the key entirely when the wrapped array 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`.
public mutating func encode<T>(
_ value: OptionalPolymorphicArrayValue<T>,
forKey key: Key
) throws where T: PolymorphicCodableStrategy {
guard value.wrappedValue != nil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// KeyedEncodingContainer+OptionalPolymorphicLossyArrayValue.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

extension KeyedEncodingContainer {
/// Encodes an `OptionalPolymorphicLossyArrayValue`, omitting the key entirely when the wrapped array 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`.
public mutating func encode<T>(
_ value: OptionalPolymorphicLossyArrayValue<T>,
forKey key: Key
) throws where T: PolymorphicCodableStrategy {
guard value.wrappedValue != nil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
//
// KeyedEncodingContainer+OptionalPolymorphicValue.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation

extension KeyedEncodingContainer {
/// Encodes an `OptionalPolymorphicValue`, omitting the key entirely when the wrapped 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`.
public mutating func encode<T>(
_ value: OptionalPolymorphicValue<T>,
forKey key: Key
) throws where T: PolymorphicCodableStrategy {
guard value.wrappedValue != nil else { return }
try value.encode(to: superEncoder(forKey: key))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ import Foundation
/// - Empty arrays are decoded as empty arrays, not `nil`
///
/// Encoding behavior:
/// - If `wrappedValue` is `nil`, encodes as `null`
/// - If `wrappedValue` is `nil`, the key is omitted (a `null` is only produced inside an unkeyed container)
/// - If `wrappedValue` contains an array, each element is encoded using the `PolymorphicType` strategy
///
@propertyWrapper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@

import Foundation

/// A property wrapper that decodes an optional array of polymorphic objects with lossy behavior for individual elements.
/// A property wrapper that decodes an optional array of polymorphic objects with lossy behavior
/// for individual elements.
///
/// This wrapper combines the optionality handling of ``OptionalPolymorphicArrayValue`` with
/// the lossy element decoding of ``PolymorphicLossyArrayValue``.
Expand All @@ -20,7 +21,8 @@ import Foundation
/// Comparison with similar wrappers:
/// - ``PolymorphicLossyArrayValue``: For required arrays that default to `[]` when missing or null
/// - ``OptionalPolymorphicArrayValue``: For optional arrays that throw on invalid elements
/// - ``DefaultEmptyPolymorphicArrayValue``: For required arrays that default to `[]` when missing or null, strict on elements
/// - ``DefaultEmptyPolymorphicArrayValue``: For required arrays that default to `[]` when missing or null,
/// strict on elements
///
/// Decoding behavior:
/// - If the key is missing or the value is `null`, `wrappedValue` is set to `nil`
Expand All @@ -29,7 +31,7 @@ import Foundation
/// - Empty arrays are decoded as empty arrays, not `nil`
///
/// Encoding behavior:
/// - If `wrappedValue` is `nil`, encodes as `null`
/// - If `wrappedValue` is `nil`, the key is omitted (a `null` is only produced inside an unkeyed container)
/// - If `wrappedValue` contains an array, each element is encoded using the `PolymorphicType` strategy
///
@propertyWrapper
Expand All @@ -42,40 +44,40 @@ public struct OptionalPolymorphicLossyArrayValue<PolymorphicType: PolymorphicCod
public let outcome: ResilientDecodingOutcome

#if DEBUG
/// Results of decoding each element in the array (DEBUG only)
let results: [Result<PolymorphicType.ExpectedType, Error>]
/// Results of decoding each element in the array (DEBUG only)
let results: [Result<PolymorphicType.ExpectedType, Error>]
#endif

public init(wrappedValue: [PolymorphicType.ExpectedType]?) {
self.wrappedValue = wrappedValue
outcome = .decodedSuccessfully
#if DEBUG
results = []
results = []
#endif
}

#if DEBUG
init(
wrappedValue: [PolymorphicType.ExpectedType]?,
outcome: ResilientDecodingOutcome,
results: [Result<PolymorphicType.ExpectedType, Error>] = []
) {
self.wrappedValue = wrappedValue
self.outcome = outcome
self.results = results
}
init(
wrappedValue: [PolymorphicType.ExpectedType]?,
outcome: ResilientDecodingOutcome,
results: [Result<PolymorphicType.ExpectedType, Error>] = []
) {
self.wrappedValue = wrappedValue
self.outcome = outcome
self.results = results
}
#else
init(wrappedValue: [PolymorphicType.ExpectedType]?, outcome: ResilientDecodingOutcome) {
self.wrappedValue = wrappedValue
self.outcome = outcome
}
init(wrappedValue: [PolymorphicType.ExpectedType]?, outcome: ResilientDecodingOutcome) {
self.wrappedValue = wrappedValue
self.outcome = outcome
}
#endif

#if DEBUG
/// The projected value providing access to decoding outcome
public var projectedValue: PolymorphicLossyArrayProjectedValue<PolymorphicType.ExpectedType> {
PolymorphicLossyArrayProjectedValue(outcome: outcome, results: results)
}
/// The projected value providing access to decoding outcome
public var projectedValue: PolymorphicLossyArrayProjectedValue<PolymorphicType.ExpectedType> {
PolymorphicLossyArrayProjectedValue(outcome: outcome, results: results)
}
#endif
}

Expand All @@ -95,29 +97,29 @@ extension OptionalPolymorphicLossyArrayValue: Decodable {

var elements = [PolymorphicType.ExpectedType]()
#if DEBUG
var results = [Result<PolymorphicType.ExpectedType, Error>]()
var results = [Result<PolymorphicType.ExpectedType, Error>]()
#endif

while !container.isAtEnd {
do {
let value = try container.decode(PolymorphicValue<PolymorphicType>.self).wrappedValue
elements.append(value)
#if DEBUG
results.append(.success(value))
results.append(.success(value))
#endif
} catch {
// Decoding processing to prevent infinite loops if decoding fails.
_ = try? container.decode(AnyDecodableValue.self)
#if DEBUG
results.append(.failure(error))
results.append(.failure(error))
#endif
}
}

#if DEBUG
self.init(wrappedValue: elements, outcome: .decodedSuccessfully, results: results)
self.init(wrappedValue: elements, outcome: .decodedSuccessfully, results: results)
#else
self.init(wrappedValue: elements, outcome: .decodedSuccessfully)
self.init(wrappedValue: elements, outcome: .decodedSuccessfully)
#endif
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
//
// OptionalDateValueOmitNilTests.swift
// KarrotCodableKit
//
// Created by Elon on 6/26/26.
// Copyright © 2026 Danggeun Market Inc. All rights reserved.
//

import Foundation
import Testing

import KarrotCodableKit

struct OptionalDateValueOmitNilTests {
private struct Fixture: Codable {
@OptionalDateValue<ISO8601Strategy> var iso8601: Date?
}

@Test
func encodingNilOmitsKey() throws {
// given
let fixture = Fixture(iso8601: nil)

// when
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(fixture)

// then - nil value is omitted, matching Apple's default Codable behavior
let jsonString = try #require(String(bytes: data, encoding: .utf8))
#expect(jsonString == "{\n\n}")
}

@Test
func encodingValuePreservesKey() throws {
// given
let fixture = Fixture(iso8601: Date(timeIntervalSince1970: 851042397))

// when
let encoder = JSONEncoder()
encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
let data = try encoder.encode(fixture)

// then - a present value is still encoded under its key
let expectResult = #"""
{
"iso8601" : "1996-12-20T00:39:57Z"
}
"""#
let jsonString = try #require(String(bytes: data, encoding: .utf8))
#expect(jsonString == expectResult)
}

@Test
func encodingDecodingNilRoundTrip() throws {
// given
let fixture = Fixture(iso8601: nil)

// when
let data = try JSONEncoder().encode(fixture)
let decoded = try JSONDecoder().decode(Fixture.self, from: data)

// then - nil is restored from the omitted key
#expect(decoded.iso8601 == nil)
}
}
Loading
Loading