Skip to content

feat: add OptionalPolymorphicLossyArrayValue property wrapper#21

Merged
ElonPark merged 5 commits intodaangn:mainfrom
KYHyeon:feature/optional-polymorphic-lossy-array
Apr 6, 2026
Merged

feat: add OptionalPolymorphicLossyArrayValue property wrapper#21
ElonPark merged 5 commits intodaangn:mainfrom
KYHyeon:feature/optional-polymorphic-lossy-array

Conversation

@KYHyeon
Copy link
Copy Markdown
Contributor

@KYHyeon KYHyeon commented Apr 6, 2026

Background (Required)

  • OptionalPolymorphicArrayValue handles array-level optionality but throws on individual element decoding failures
  • PolymorphicLossyArrayValue silently skips invalid elements but the array itself is non-optional (falls back to [])
  • There was no wrapper that combines both behaviors — needed for cases where the array itself can be nil and individual element failures should be skipped

Changes (Required)

OptionalPolymorphicLossyArrayValue property wrapper

  • Combines optionality handling of OptionalPolymorphicArrayValue with lossy element decoding of PolymorphicLossyArrayValue
  • Missing key → nil (.keyNotFound), null value → nil (.valueWasNil), present array → lossy decode
  • Conforms to Decodable / Encodable / Equatable / Hashable / Sendable
  • Supports ResilientDecodingOutcome tracking via $projectedValue in DEBUG builds

KeyedDecodingContainer extension

  • decode(_:forKey:) / decodeIfPresent(_:forKey:) overloads for the new property wrapper

@PolymorphicCodableStrategyProviding macro

  • Added OptionalPolymorphicLossyArray typealias generation

Docs

  • Added OptionalPolymorphicLossyArrayValue to README and CLAUDE.md

Testing Approach (Required)

  • 8 test cases added for OptionalPolymorphicLossyArrayValue:
    • Valid array decoding, null value, missing key, empty array
    • Partial invalid elements skipped, all invalid elements → empty array (not nil)
    • Encoding, encoding-decoding round trip
  • Macro expansion tests updated to include OptionalPolymorphicLossyArray typealias
  • All 151+ existing tests continue to pass

Review Notes

  • Follows the same patterns as existing PolymorphicLossyArrayValue and OptionalPolymorphicArrayValue
  • Reuses PolymorphicLossyArrayProjectedValue for projected value, consistent with PolymorphicLossyArrayValue

Summary by CodeRabbit

  • New Features

    • Added optional lossy polymorphic-array support for resilient decoding/encoding of optional array fields with per-element failure tolerance and outcome reporting.
    • Macro-generated alias added to expose the new optional lossy array variant.
  • Documentation

    • Updated README and docs/examples to demonstrate optional lossy polymorphic array usage and nil-on-absent behavior.
  • Tests

    • Added tests and test helpers covering decoding, lossy-skipping behavior, encoding, and encode→decode round-trips.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 6, 2026

📝 Walkthrough

Walkthrough

Adds an optional lossy polymorphic-array property wrapper and supporting APIs: OptionalPolymorphicLossyArrayValue, keyed decoding helpers, a macro-emitted typealias, README/CLAUDE docs updates, tests, and test helpers to demonstrate optional per-element lossy decoding of polymorphic arrays.

Changes

Cohort / File(s) Summary
Documentation & Examples
CLAUDE.md, README.md
Documented the new OptionalPolymorphicLossyArrayValue and updated README example to show @...OptionalPolymorphicLossyArray producing an optional lossy polymorphic array ([ViewItem]?).
Property Wrapper Implementation
Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift
Added public OptionalPolymorphicLossyArrayValue<PolymorphicType>: wrappedValue: [PolymorphicType.ExpectedType]?, outcome: ResilientDecodingOutcome, Decodable/Encodable with per-element lossy decoding, DEBUG results tracking, and conditional Equatable/Hashable/Sendable conformances.
Decoder Extension
Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedDecodingContainer+OptionalPolymorphicLossyArrayValue.swift
Added KeyedDecodingContainer overloads: decode(_: OptionalPolymorphicLossyArrayValue<T>.Type, forKey:) and decodeIfPresent(_: OptionalPolymorphicLossyArrayValue<T>.Type, forKey:) handling missing keys, explicit null, and constructing the wrapper via a decoder (constrained to T: PolymorphicCodableStrategy).
Macro Generation
Sources/KarrotCodableKitMacros/PolymorphicCodableMacros/PolymorphicCodableStrategyMacro.swift
Macro MemberMacro expansion now emits typealias OptionalPolymorphicLossyArray = OptionalPolymorphicLossyArrayValue<...> alongside other generated aliases.
Tests
Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift
Added tests covering decoding/encoding scenarios: heterogeneous arrays, explicit null, missing keys, empty arrays, lossy skipping of invalid elements, all-invalid arrays, exact JSON encoding, and encode→decode round-trips.
Test Helpers & Macro Tests
Tests/KarrotCodableKitTests/PolymorphicCodable/TestDoubles/PolymorphicValueCodableDummy.swift, Tests/KarrotCodableMacrosTests/.../PolymorphicCodableStrategyProvidingMacroTests.swift
Added OptionalPolymorphicLossyArrayDummyResponse test struct with @DummyNotice.OptionalPolymorphicLossyArray fields; updated macro-expansion tests to expect the new generated OptionalPolymorphicLossyArray typealias.

Sequence Diagram(s)

sequenceDiagram
    participant Client as Client Code
    participant KDC as KeyedDecodingContainer
    participant Wrapper as OptionalPolymorphicLossyArrayValue
    participant Poly as PolymorphicValue Decoder
    participant Acc as Accumulator

    Client->>KDC: decode(OptionalPolymorphicLossyArrayValue, forKey:)
    activate KDC

    KDC->>KDC: check key exists
    alt key missing
        KDC->>Wrapper: init(wrappedValue: nil, outcome: .keyNotFound)
    else key present
        KDC->>KDC: check value is nil
        alt value is null
            KDC->>Wrapper: init(wrappedValue: nil, outcome: .valueWasNil)
        else value is array
            KDC->>KDC: obtain superDecoder(forKey:)
            KDC->>Wrapper: create unkeyed container
            loop for each element
                Wrapper->>Poly: try decode(PolymorphicValue<T>)
                alt success
                    Poly->>Acc: append decoded wrappedValue
                else failure
                    Wrapper->>Wrapper: catch error, decode AnyDecodableValue to advance (skip)
                    Wrapper->>Wrapper: record failure (DEBUG)
                end
            end
            Acc->>Wrapper: set wrappedValue = accumulated results
            Wrapper->>Wrapper: set outcome = decodedSuccessfully / lossy
        end
    end

    KDC->>Client: return OptionalPolymorphicLossyArrayValue
    deactivate KDC
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Suggested labels

Feature, Docs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately and specifically describes the main change: adding a new OptionalPolymorphicLossyArrayValue property wrapper. It is concise, clear, and follows conventional commit format.
Description check ✅ Passed The description fully covers all required sections with substantial detail: Background explains the problem, Changes detail the implementation, Testing Approach covers test cases, and Review Notes provide helpful context. All template sections are properly addressed.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@KYHyeon KYHyeon marked this pull request as draft April 6, 2026 08:20
@KYHyeon KYHyeon force-pushed the feature/optional-polymorphic-lossy-array branch from 426fc97 to f2b0a2b Compare April 6, 2026 08:24
@KYHyeon KYHyeon marked this pull request as ready for review April 6, 2026 08:25
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift`:
- Around line 85-114: The init(from decoder: Decoder) in
OptionalPolymorphicLossyArrayValue currently calls decoder.unkeyedContainer()
immediately which fails when the incoming JSON is explicit null; change it to
first use decoder.singleValueContainer() and check decodeNil() like
OptionalPolymorphicArrayValue does, and if nil set wrappedValue = nil and
outcome = .valueWasNil and return, otherwise fall through to obtain an unkeyed
container and proceed with the existing lossy decoding loop (preserve debug
results handling and the use of PolymorphicValue<PolymorphicType> and
AnyDecodableValue).

In
`@Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift`:
- Around line 204-225: The test currently asserts equality against a
pretty-printed JSON string (expect(jsonString == expectResult)), which is
brittle; instead parse both the encoded data (or jsonString) and the expected
JSON literal into native JSON objects and compare those for equality. Locate the
test variables response, expectResult, the JSONEncoder setup, the encoded data,
and the final expect(...) assertion in
OptionalPolymorphicLossyArrayValueTests.swift and replace the string equality
assertion with a semantic comparison of deserialized JSON (e.g., decode into
Dictionary/Array or use JSONSerialization/JSONDecoder) so formatting/ordering
differences no longer break the test.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: 4962bc2a-cb0b-4b8c-99ae-3a37b348ff42

📥 Commits

Reviewing files that changed from the base of the PR and between ac97bc0 and 426fc97.

📒 Files selected for processing (8)
  • CLAUDE.md
  • README.md
  • Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedDecodingContainer+OptionalPolymorphicLossyArrayValue.swift
  • Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift
  • Sources/KarrotCodableKitMacros/PolymorphicCodableMacros/PolymorphicCodableStrategyMacro.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/TestDoubles/PolymorphicValueCodableDummy.swift
  • Tests/KarrotCodableMacrosTests/PolymorphicCodableMacrosTests/PolymorphicCodableStrategyProvidingMacroTests.swift

Comment on lines +204 to +225
let expectResult = #"""
{
"notices1" : [
{
"description" : "test",
"icon" : "test_icon",
"type" : "callout"
}
],
"notices2" : null
}
"""#

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

// then
let jsonString = String(decoding: data, as: UTF8.self)
#expect(jsonString == expectResult)
}
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

[MEDIUM] Prefer semantic JSON comparison over exact pretty-printed string comparison.

Comparing full pretty JSON text is brittle. Compare parsed JSON objects instead to keep the test stable across formatting differences.

Suggested refactor
-    let jsonString = String(decoding: data, as: UTF8.self)
-    `#expect`(jsonString == expectResult)
+    let expectedData = try `#require`(expectResult.data(using: .utf8))
+    let actualJSON = try JSONSerialization.jsonObject(with: data) as? NSDictionary
+    let expectedJSON = try JSONSerialization.jsonObject(with: expectedData) as? NSDictionary
+    `#expect`(actualJSON == expectedJSON)

As per coding guidelines, "Prioritize real-world usage scenario coverage" and "Validate JSON encoding/decoding scenarios cover real-world use cases."

🧰 Tools
🪛 SwiftLint (0.63.2)

[Warning] 223-223: Prefer failable String(bytes:encoding:) initializer when converting Data to String

(optional_data_string_conversion)

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift`
around lines 204 - 225, The test currently asserts equality against a
pretty-printed JSON string (expect(jsonString == expectResult)), which is
brittle; instead parse both the encoded data (or jsonString) and the expected
JSON literal into native JSON objects and compare those for equality. Locate the
test variables response, expectResult, the JSONEncoder setup, the encoded data,
and the final expect(...) assertion in
OptionalPolymorphicLossyArrayValueTests.swift and replace the string equality
assertion with a semantic comparison of deserialized JSON (e.g., decode into
Dictionary/Array or use JSONSerialization/JSONDecoder) so formatting/ordering
differences no longer break the test.

Copy link
Copy Markdown
Contributor Author

@KYHyeon KYHyeon Apr 6, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test uses JSONEncoder with [.prettyPrinted, .sortedKeys], which produces deterministic output per
https://developer.apple.com/documentation/foundation/jsonencoder/outputformatting/2919670-sortedkeys:

.sortedKeys — Produce JSON with dictionary keys sorted in lexicographic order.
https://github.com/swiftlang/swift-foundation/blob/main/Sources/FoundationEssentials/JSON/JSONEncoder.swift

This library targets Apple platforms only (Package.swift — .macOS(.v11), .iOS(.v13), ...), so the
swiftlang/swift-corelibs-foundation#4702 is not a concern. The same pattern is used consistently across the test suite
(OptionalPolymorphicArrayValueTests, UnnestedPolymorphicCodableTests).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@KYHyeon, that's a valid and well-reasoned rebuttal. You're right on all counts:

  1. Deterministic output: .sortedKeys guarantees lexicographic key ordering, and .prettyPrinted guarantees consistent whitespace, making the encoded output fully deterministic on Apple platforms.
  2. Platform scope: Since the library targets Apple platforms exclusively, the swift-corelibs-foundation inconsistency (#4702) doesn't apply.
  3. Consistency: The same pattern is already established in OptionalPolymorphicArrayValueTests and UnnestedPolymorphicCodableTests, making this the idiomatic approach in this test suite.

My suggestion to use JSONSerialization for semantic comparison was overly cautious here. The string comparison approach is perfectly appropriate. Happy to withdraw that comment.


✏️ Learnings added
Learnt from: KYHyeon
Repo: daangn/KarrotCodableKit PR: 21
File: Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift:204-225
Timestamp: 2026-04-06T08:56:31.954Z
Learning: In KarrotCodableKit (Swift, Apple-platforms-only library targeting macOS(.v11), iOS(.v13), etc.), JSON encoding tests intentionally use exact pretty-printed string comparison with `JSONEncoder` configured with `.prettyPrinted` and `.sortedKeys`. This produces deterministic output on Apple platforms (swift-corelibs-foundation cross-platform sortedKeys inconsistency in issue `#4702` is not a concern). This pattern is used consistently across the test suite (e.g., OptionalPolymorphicArrayValueTests, UnnestedPolymorphicCodableTests). Do not suggest replacing it with semantic JSON comparison via JSONSerialization.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift (1)

223-224: ⚠️ Potential issue | 🟡 Minor

[MEDIUM] Prefer semantic JSON comparison over exact pretty-printed string equality.

This assertion is fragile to formatting differences; compare parsed JSON objects instead.

Suggested refactor
-    let jsonString = String(decoding: data, as: UTF8.self)
-    `#expect`(jsonString == expectResult)
+    let expectedData = try `#require`(expectResult.data(using: .utf8))
+    let actualJSON = try JSONSerialization.jsonObject(with: data) as? NSDictionary
+    let expectedJSON = try JSONSerialization.jsonObject(with: expectedData) as? NSDictionary
+    `#expect`(actualJSON == expectedJSON)

As per coding guidelines, "Validate JSON encoding/decoding scenarios cover real-world use cases."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift`
around lines 223 - 224, The test currently asserts raw pretty-printed string
equality via jsonString and expectResult which is brittle; instead parse both
into JSON objects and assert semantic equality. Replace the string equality
check (the line using jsonString and expectResult) with code that uses
JSONSerialization.jsonObject(with: data, options: []) for the produced data and
similarly parse expectResult.data(using: .utf8), then compare the resulting
objects (e.g., as NSDictionary/NSArray with isEqual) or use JSONDecoder to
decode into a concrete Codable structure and compare; update the assertion to
assert the parsed objects are equal (referencing jsonString, expectResult, and
the data variable in OptionalPolymorphicLossyArrayValueTests).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In
`@Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift`:
- Around line 223-224: The test currently asserts raw pretty-printed string
equality via jsonString and expectResult which is brittle; instead parse both
into JSON objects and assert semantic equality. Replace the string equality
check (the line using jsonString and expectResult) with code that uses
JSONSerialization.jsonObject(with: data, options: []) for the produced data and
similarly parse expectResult.data(using: .utf8), then compare the resulting
objects (e.g., as NSDictionary/NSArray with isEqual) or use JSONDecoder to
decode into a concrete Codable structure and compare; update the assertion to
assert the parsed objects are equal (referencing jsonString, expectResult, and
the data variable in OptionalPolymorphicLossyArrayValueTests).

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: f2e2228e-a5a8-441d-a88a-a7d56e4588ca

📥 Commits

Reviewing files that changed from the base of the PR and between 426fc97 and f2b0a2b.

📒 Files selected for processing (6)
  • CLAUDE.md
  • README.md
  • Sources/KarrotCodableKitMacros/PolymorphicCodableMacros/PolymorphicCodableStrategyMacro.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/TestDoubles/PolymorphicValueCodableDummy.swift
  • Tests/KarrotCodableMacrosTests/PolymorphicCodableMacrosTests/PolymorphicCodableStrategyProvidingMacroTests.swift

…from:)

Add singleValueContainer decodeNil check before unkeyedContainer call
to prevent decoding failure when JSON value is explicit null, consistent
with OptionalPolymorphicArrayValue.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift`:
- Around line 126-135: The encode path in
OptionalPolymorphicLossyArrayValue.encode(to:) currently maps wrappedValue to an
intermediate [PolymorphicValue<PolymorphicType>] which allocates memory;
instead, open an unkeyedContainer from the provided Encoder when wrappedValue is
non-nil and iterate the array encoding each
PolymorphicValue<PolymorphicType>(wrappedValue: element) directly into the
container (call container.encode(...) per element) to avoid the temporary array
allocation, preserving the existing nil branch that writes
encoder.singleValueContainer().encodeNil().
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yml

Review profile: ASSERTIVE

Plan: Pro

Run ID: b6b3bf30-489d-444d-a4db-b36ba396f7c6

📥 Commits

Reviewing files that changed from the base of the PR and between f2b0a2b and f9effd2.

📒 Files selected for processing (1)
  • Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift

Copy link
Copy Markdown
Member

@ElonPark ElonPark left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍👍

@KYHyeon
Copy link
Copy Markdown
Contributor Author

KYHyeon commented Apr 6, 2026

Thank you for the review, @ElonPark! Could you please merge this when you get a chance?

@ElonPark ElonPark merged commit 59b2d2a into daangn:main Apr 6, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants