Skip to content

Ensure deterministic ordering for Dictionary keys#22

Closed
rickhohler wants to merge 5 commits intotoon-format:mainfrom
rickhohler:fix/deterministic-key-sorting
Closed

Ensure deterministic ordering for Dictionary keys#22
rickhohler wants to merge 5 commits intotoon-format:mainfrom
rickhohler:fix/deterministic-key-sorting

Conversation

@rickhohler
Copy link
Copy Markdown
Contributor

@rickhohler rickhohler commented Feb 5, 2026

Separated from #20 per review feedback.

@mattt mattt force-pushed the fix/deterministic-key-sorting branch from 60a3cfa to 7c9543c Compare February 6, 2026 13:00
@mattt mattt self-requested a review as a code owner February 6, 2026 13:00
Comment on lines +813 to +814
// Swift Dictionary encoding uses an internal DictionaryCodingKey; detect it to sort keys deterministically.
private let isDictionaryCodingKey = String(reflecting: Key.self).contains("DictionaryCodingKey")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Kind of hacky, but it lets us have nice behavior where Dictionary keys are sorted but struct fields retain ordering.

@mattt mattt changed the title fix: Ensure deterministic dictionary key ordering Ensure deterministic ordering for Dictionary keys Feb 6, 2026
// MARK: - Factory

/// Creates a `Value` from an arbitrary value.
static func from(_ value: Any) -> Value {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Turns out, we weren't using this anywhere 🫠

@mattt
Copy link
Copy Markdown
Collaborator

mattt commented Feb 6, 2026

Thanks for your help with this, @rickhohler. I think I ended up rewriting all of the original PR, but the good news is that we now have deterministic ordering when encoding objects / key-value pairs (sorted for dictionaries, in declaration / encoding order for structures)

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR implements deterministic alphabetical ordering for Dictionary keys in the TOONEncoder to ensure consistent serialization output. This was separated from PR #20 which addressed both dictionary ordering and negative zero preservation.

Changes:

  • Adds detection logic to identify Swift Dictionary encoding via reflection on internal CodingKey type
  • Sorts dictionary keys alphabetically when detected, while preserving field order for regular Encodable structs
  • Removes unused Value.from(_:) factory method that previously handled dictionaries differently
  • Updates README to document the dictionary key sorting behavior
  • Adds comprehensive test with 30 permutations to verify deterministic ordering

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
Sources/ToonFormat/Encoder.swift Adds isDictionaryCodingKey detection and applies alphabetical sorting to dictionary keys in finishEncoding and deinit
Sources/ToonFormat/Value.swift Removes unused Value.from(_:) static factory method (92 lines removed)
Tests/ToonFormatTests/EncoderTests.swift Adds dictionaryKeyOrderingIsDeterministic test with 30 rotation permutations
README.md Documents that Dictionary keys are sorted while Encodable field order is preserved

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.


private var container: [String: Value] = [:]
private var keyOrder: [String] = [] // Track insertion order
// Swift Dictionary encoding uses an internal DictionaryCodingKey; detect it to sort keys deterministically.
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The detection of Dictionary encoding via String reflection is fragile and relies on internal Swift implementation details. The string "DictionaryCodingKey" is an internal Swift type name that could change in future Swift versions, breaking this functionality silently.

Consider these alternatives:

  1. Document this as a known limitation that depends on Swift internals
  2. Add a test that explicitly verifies the reflection string contains "DictionaryCodingKey" when encoding a Dictionary, so that any Swift version upgrade that breaks this will be caught
  3. Explore whether there's a more robust way to differentiate Dictionary encoding from struct encoding (though this may not be possible with the Codable API)

Given that this is a critical feature for deterministic output, it would be prudent to have explicit test coverage that verifies the detection mechanism is working as expected.

Suggested change
// Swift Dictionary encoding uses an internal DictionaryCodingKey; detect it to sort keys deterministically.
/// Heuristic: Swift's `Dictionary` encoding uses an internal `DictionaryCodingKey` type.
///
/// We detect this by checking whether `String(reflecting: Key.self)` contains
/// the substring `"DictionaryCodingKey"`, and if so, we treat the container
/// as a dictionary and sort its keys deterministically.
///
/// This relies on Swift's internal implementation details and is therefore
/// inherently fragile: if the type name changes in a future Swift version,
/// this detection will stop working and dictionary key ordering may become
/// non-deterministic again without a compile-time error.
///
/// This limitation should be documented and covered by tests that assert
/// the presence of `"DictionaryCodingKey"` when encoding a `Dictionary`,
/// so that Swift upgrades which change the internal type name are caught.

Copilot uses AI. Check for mistakes.
Comment on lines +1100 to +1107
let finalKeyOrder = isDictionaryCodingKey ? container.keys.sorted() : keyOrder
encoder.storage.append(.object(container, keyOrder: finalKeyOrder))
}

deinit {
// Ensure the container is finished when it goes out of scope
encoder.storage.append(.object(container, keyOrder: keyOrder))
let finalKeyOrder = isDictionaryCodingKey ? container.keys.sorted() : keyOrder
encoder.storage.append(.object(container, keyOrder: finalKeyOrder))
Copy link

Copilot AI Feb 6, 2026

Choose a reason for hiding this comment

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

The same logic for determining finalKeyOrder is duplicated in both finishEncoding and deinit. Consider extracting this into a computed property or private method to reduce duplication and improve maintainability. For example:

private var finalKeyOrder: [String] {
    isDictionaryCodingKey ? container.keys.sorted() : keyOrder
}

This would make both methods simpler and ensure the logic stays consistent if it needs to be updated in the future.

Copilot uses AI. Check for mistakes.
@mattt
Copy link
Copy Markdown
Collaborator

mattt commented Feb 6, 2026

Closing in favor of #25

@mattt mattt closed this Feb 6, 2026
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.

3 participants