Skip to content

feat: omit nil optional fields on encode (IOS-4537)#23

Merged
ElonPark merged 8 commits into
mainfrom
feature/elon/IOS-4537-omit-nil-field-on-encode
Jun 26, 2026
Merged

feat: omit nil optional fields on encode (IOS-4537)#23
ElonPark merged 8 commits into
mainfrom
feature/elon/IOS-4537-omit-nil-field-on-encode

Conversation

@ElonPark

@ElonPark ElonPark commented Jun 26, 2026

Copy link
Copy Markdown
Member

Overview

Changes the encoding behavior of Optional property wrappers/macros so that a nil value omits the key entirely, matching Apple's default Codable behavior — instead of writing an explicit null via encodeNil().

Background

Swift's synthesized Codable omits the key for nil optional properties (via encodeIfPresent). KarrotCodableKit's Optional wrappers instead emitted an explicit null through singleValueContainer().encodeNil(), diverging from that behavior.

Note: simply removing the encodeNil() call inside a wrapper's encode(to:) does not omit the key — it leaves an empty object {} behind. Key omission can only be decided at the parent container level, i.e. whether encode(_:forKey:) is invoked at all.

Changes

Each Optional wrapper gains a KeyedEncodingContainer.encode(_:forKey:) overload that skips the key when wrappedValue == nil. This is exactly symmetric with the existing decode(_:forKey:) overload (contains(key) check). Macros (@CustomCodable synthesis / @UnnestedPolymorphic) call this container overload automatically, so no macro changes are required.

Covered 7 wrappers (split into one commit per wrapper):

  • OptionalPolymorphicValue
  • LossyOptionalPolymorphicValue
  • OptionalPolymorphicArrayValue
  • OptionalPolymorphicLossyArrayValue
  • OptionalDateValue
  • OptionalLosslessValue
  • LossyOptional — constrained to DefaultCodable<DefaultNilStrategy> so other DefaultCodable wrappers (@DefaultFalse, @DefaultEmptyArray, ...) are unaffected

Behavior

  • nil → key omitted
  • present value → encoded as before
  • empty array [] → preserved (distinct from nil)
  • unkeyed context (array element) → still null (same as Apple's [T?])
  • round-trip: omitted key → keyNotFound on decode → restored to nil

⚠️ Breaking Change

Encoded output now omits the key for nil optional fields instead of writing "key": null. Consumers that distinguish explicit null from key absence (e.g. PATCH semantics where null = "delete field") may be affected.

Tests

  • Debug: XCTest 194 + swift-testing 165, 0 failures
  • Release: XCTest 194 + swift-testing 162, 0 failures
  • Added per-wrapper nil-omission + round-trip tests, plus empty-array ([]) preservation regression tests

https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q

Summary by CodeRabbit

  • New Features

    • Optional values in JSON encoding now omit missing fields instead of writing null for several supported value types.
    • Added coverage for polymorphic arrays, lossy arrays, polymorphic values, lossless values, and dates.
  • Bug Fixes

    • Improved round-trip behavior so omitted fields decode back to nil correctly.
    • Preserved empty arrays during encoding while still omitting missing values.
  • Tests

    • Added and updated tests to verify empty-object output, field omission, and encoding/decoding consistency.

ElonPark added 7 commits June 26, 2026 17:12
Add a KeyedEncodingContainer.encode(_:forKey:) overload that skips the key when wrappedValue is nil, matching Apple's default Codable behavior (nil optionals are omitted, not encoded as an explicit null). Mirrors the existing decode(_:forKey:) overload, so round-trips restore nil.

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
Add the nil-omission encoding overload for the lossy variant, consistent with OptionalPolymorphicValue.

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
Add the nil-omission encoding overload for the optional array wrapper. An empty array ([]) is non-nil and stays as []; only nil omits the key. Also update the now-stale 'encodes as null' doc.

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
Add the nil-omission encoding overload for the optional lossy array wrapper. Empty arrays are preserved; only nil omits the key. Also update the stale doc and wrap two over-long doc lines.

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
Add the nil-omission encoding overload for OptionalDateValue (BetterCodable), consistent with the polymorphic optional wrappers.

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
Add the nil-omission encoding overload for OptionalLosslessValue (BetterCodable).

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
Add the nil-omission encoding overload for LossyOptional. Constrained to DefaultCodable<DefaultNilStrategy> so other DefaultCodable wrappers (DefaultFalse, DefaultEmptyArray, ...) keep encoding their non-optional default value.

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
@coderabbitai

coderabbitai Bot commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ElonPark, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 41 minutes and 5 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based credits.

🚦 How do rate limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3ad643f4-1254-48dc-bffc-4d8ac9327a0b

📥 Commits

Reviewing files that changed from the base of the PR and between 59e0f9f and 8315fbc.

📒 Files selected for processing (1)
  • Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicArrayValueTests.swift

Walkthrough

Adds keyed encoding overloads for several optional wrapper types so nil values are omitted from encoded keyed containers. Updates wrapper docs and debug-only plumbing for optional polymorphic arrays. Expands tests to cover nil omission, preserved empty arrays, and round-trip decoding.

Changes

Optional nil-encoding behavior

Layer / File(s) Summary
BetterCodable keyed encoders
Sources/KarrotCodableKit/BetterCodable/DateValue/KeyedEncodingContainer+OptionalDateValue.swift, Sources/KarrotCodableKit/BetterCodable/LosslessValue/KeyedEncodingContainer+OptionalLosslessValue.swift, Sources/KarrotCodableKit/BetterCodable/LossyValue/KeyedEncodingContainer+LossyOptional.swift, Tests/KarrotCodableKitTests/BetterCodable/.../OptionalDateValueOmitNilTests.swift, Tests/KarrotCodableKitTests/BetterCodable/.../OptionalLosslessValueOmitNilTests.swift, Tests/KarrotCodableKitTests/BetterCodable/.../LossyOptionalOmitNilTests.swift
Adds keyed encode(_:forKey:) overloads that skip nil wrapped values, with tests for omitted keys and round-trip decoding.
Optional polymorphic values
Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+LossyOptionalPolymorphicValue.swift, Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+OptionalPolymorphicValue.swift, Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalValue/LossyOptionalPolymorphicValueTests.swift, Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalValue/OptionalPolymorphicValueTests.swift
Adds keyed encoders for optional polymorphic value wrappers and updates tests to expect omitted nil keys and decode round-trips.
Optional polymorphic arrays
Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+OptionalPolymorphicArrayValue.swift, Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+OptionalPolymorphicLossyArrayValue.swift, Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicArrayValue.swift, Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift, Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicArrayValueTests.swift, Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift
Adds keyed encoders for optional polymorphic arrays, rewords nil-encoding docs, adjusts DEBUG-only projected-value storage, and updates array tests for omitted nil keys and preserved empty arrays.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • daangn/KarrotCodableKit#21: Adds the closely related KeyedEncodingContainer.encode(_:forKey:) path for OptionalPolymorphicLossyArrayValue, which matches the optional-wrapper encoding behavior expanded here.

Suggested labels

Feature, Breaking Changes

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 25.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise and accurately summarizes the main change: omitting nil optional fields during encoding.
Description check ✅ Passed The description covers background, changes, testing, and review notes, though it uses custom headings instead of the exact template.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/elon/IOS-4537-omit-nil-field-on-encode

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.

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicArrayValueTests.swift`:
- Around line 209-227: The current tests cover keyed omission and empty-array
preservation, but they miss the unkeyed null behavior required by the PR
contract. Add a focused regression test around OptionalPolymorphicArrayValue’s
encode(to:) and decode paths in an unkeyed array context, verifying that a nil
element is encoded as null and round-trips back to nil. Use the existing
OptionalPolymorphicArrayDummyResponse / OptionalPolymorphicArrayValue test area
to keep the new case close to the current polymorphic array coverage.
🪄 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: a7d93427-cfd1-4a68-9b1d-f7603459ce43

📥 Commits

Reviewing files that changed from the base of the PR and between 4aade81 and 59e0f9f.

📒 Files selected for processing (16)
  • Sources/KarrotCodableKit/BetterCodable/DateValue/KeyedEncodingContainer+OptionalDateValue.swift
  • Sources/KarrotCodableKit/BetterCodable/LosslessValue/KeyedEncodingContainer+OptionalLosslessValue.swift
  • Sources/KarrotCodableKit/BetterCodable/LossyValue/KeyedEncodingContainer+LossyOptional.swift
  • Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+LossyOptionalPolymorphicValue.swift
  • Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+OptionalPolymorphicArrayValue.swift
  • Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+OptionalPolymorphicLossyArrayValue.swift
  • Sources/KarrotCodableKit/PolymorphicCodable/Extensions/KeyedEncodingContainer+OptionalPolymorphicValue.swift
  • Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicArrayValue.swift
  • Sources/KarrotCodableKit/PolymorphicCodable/OptionalPolymorphicLossyArrayValue.swift
  • Tests/KarrotCodableKitTests/BetterCodable/DateValue/OptionalDateValueOmitNilTests.swift
  • Tests/KarrotCodableKitTests/BetterCodable/LosslessValue/OptionalLosslessValueOmitNilTests.swift
  • Tests/KarrotCodableKitTests/BetterCodable/LossyValue/LossyOptionalOmitNilTests.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicArrayValueTests.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalPolymorphicLossyArrayValueTests.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalValue/LossyOptionalPolymorphicValueTests.swift
  • Tests/KarrotCodableKitTests/PolymorphicCodable/OptionalValue/OptionalPolymorphicValueTests.swift

@ElonPark ElonPark self-assigned this Jun 26, 2026
@ElonPark ElonPark added the Breaking Changes Breaking Changes label Jun 26, 2026
…Value

In an unkeyed container (array element) there is no key to omit, so a nil wrapper is encoded as an explicit null, matching Apple's [T?] behavior. This locks down the wrapper's encode(to:) path that the keyed encode(_:forKey:) overload does not exercise.

Claude-Session: https://claude.ai/code/session_01BSXaRrJaLrjWQaiJSMy45Q
@ElonPark ElonPark merged commit 088c813 into main Jun 26, 2026
4 checks passed
@ElonPark ElonPark deleted the feature/elon/IOS-4537-omit-nil-field-on-encode branch June 26, 2026 08:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Breaking Changes Breaking Changes

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant