Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

LossyDictionary: added failing test and fix for key conversion issue #51

Merged
merged 1 commit into from
May 2, 2022

Conversation

NachoSoto
Copy link
Contributor

@NachoSoto NachoSoto commented Apr 27, 2022

The property wrapper LossyDictionary incorrectly decodes keys with KeyDecodingStrategy.convertFromSnakeCase.
The test simply encodes and decodes a sample data, to illustrate that the conversion isn't idempotent.

Keys were being converted to camel case, which is inconsistent from the default behavior that container.decode([String: Value].self, forKey: key) would have.
The reason for this is some implementation detail in Foundation that ignores the keyDecodingStrategy when decoding "raw" dictionaries versus property names.

To deal with this, this decodes the strings first as "raw", and then matches them to the corresponding converted key.

let reencodedFixture = try decoder.decode(Fixture.self, from: encoder.encode(fixture))

XCTAssertEqual(reencodedFixture, fixture)
XCTAssertEqual(Set(reencodedFixture.stringToInt.keys), ["snake_case"])
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This becomes snakeCase. Removing @LossyDictionary from Fixture's definition makes the test pass.

@NachoSoto NachoSoto changed the title LossyDictionary: added failing test for key convertion issue LossyDictionary: added failing test and fix for key conversion issue Apr 27, 2022
intToString: [:]
)

let reencodedFixture = try decoder.decode(Fixture.self, from: encoder.encode(fixture))
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Encode and decode to ensure that fixture == reencodedFixture.

Copy link
Owner

@marksands marksands left a comment

Choose a reason for hiding this comment

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

Thanks for the report and the fix! I'll try to get this in soon. I have some local work in progress to fix another encoding strategy from JSONEncoder. Seems I need to sift through each strategy and make sure everything's working. 😬

Sources/BetterCodable/LossyDictionary.swift Outdated Show resolved Hide resolved
@NachoSoto
Copy link
Contributor Author

Ideally that manual key conversion wouldn't be necessary, but I haven't been able to figure out how to avoid it.

@NachoSoto
Copy link
Contributor Author

I've changed the workaround to not use that method and instead look at the original unconverted keys.

Comment on lines 87 to 89
"snake_case.with.dots_99.and_numbers": 1,
"dots.and_2.00.1_numbers": 2,
"key.1": 3
Copy link
Contributor Author

Choose a reason for hiding this comment

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

These were failing tests before and/or with the other workaround.

The property wrapper `LossyDictionary` incorrectly decodes keys with `KeyDecodingStrategy.convertFromSnakeCase`.
The test simply encodes and decodes a sample data, to illustrate that the conversion isn't idempotent.

Keys were being converted to camel case, which is inconsistent from the default behavior that `container.decode([String: Value].self, forKey: key)` would have.
The reason for this is some implementation detail in `Foundation` that ignores the `keyDecodingStrategy` when decoding "raw" dictionaries versus property names.

To deal with this, this decodes the strings first as "raw", and then matches them to the corresponding converted key.

return zip(
container.allKeys.sorted(by: { $0.stringValue < $1.stringValue }),
keys.sorted()
Copy link
Owner

Choose a reason for hiding this comment

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

I understand why this is necessary, but how robust is it? Can you think of a scenario where the sorting would be different when converting from snake case? I'd hate for there to be an edge case that's missed.

I think the answer is "no", but is it possible to route through this only when the key strategy isn't the default?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I couldn't think of a way, but I agree it's not ideal.
Unfortunately the Decoder protocol doesn't expose that. I went through the whole Swift implementation to understand how it uses private types in a way that we can't.

Copy link
Owner

Choose a reason for hiding this comment

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

Thanks for the due diligence. I'll play around with this tonight and try and button up some other in flight work.

Copy link
Owner

@marksands marksands left a comment

Choose a reason for hiding this comment

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

I'm going to merge this because I haven't come up with a better solution. If this exposes problems, then hopefully they're reported and then I can take a closer look at it. I have an outstanding fix for a DefaultCodable encoding bug that I need to add tests for. If you would prefer a release or tag for this, though, let me know so you're not blocked.

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