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

[SR-7788] Enum with String or Int RawRepresentable not encoded / decoded properly #3690

Open
swift-ci opened this issue May 27, 2018 · 13 comments
Assignees

Comments

@swift-ci
Copy link
Contributor

Previous ID SR-7788
Radar rdar://problem/40599304
Original Reporter Zarghol (JIRA User)
Type Bug
Additional Detail from JIRA
Votes 21
Component/s Foundation
Labels Bug
Assignee @itaiferber
Priority Medium

md5: fed4eeeb518f5736bc295db089a6027c

Issue Description:

description

When using an enum value that is RawRepresentable with String or Int, the json encoded is an an array of alternating keys and values, Instead of a Dictionary.

This bug is first discussed here : https://forums.swift.org/t/json-encoding-decoding-weird-encoding-of-dictionary-with-enum-values

repro

reproduced in Swift 4.1, (not tested in Swift 4.2)

let json = "{\"key\": \"value\"}" 
enum Key: String, Codable { 
   case key 
} 
let jsonData = Data(json.utf8)
let dataToEncode = [Key.key: "value"]
do { 
   let decoded = try JSONDecoder().decode([Key: String].self, from: jsonData)
   print(decoded)
   let encoded = try JSONEncoder().encode(dataToEncode)
   print(String(data: encoded, encoding: .utf8)!)
} catch { 
   print(error) 
}

expected

Encode and Decode a Dictionary with the enum value rawValue as the key.

actual

gives the following errors for the decoding part

typeMismatch(Swift.Array<Any>, Swift.DecodingError.Context(codingPath: [], debugDescription: "Expected to decode Array<Any> but found a dictionary instead.", underlyingError: nil))

give the following result for the encoding part

[ "key", value ]

@swift-ci
Copy link
Contributor Author

Comment by Clément Nonn (JIRA)

The code leading to this behavior is located here : apple/swift/blob/d2085d8b0ed69e40a10e555669bb6cc9b450d0b3/stdlib/public/core/Codable.swift.gyb#L1967

@itaiferber
Copy link
Contributor

@swift-ci create

@swift-ci
Copy link
Contributor Author

Comment by Mattias Persson (JIRA)

This issue still exists in Swift 5. For me it's a pretty big problem.

@swift-ci
Copy link
Contributor Author

Comment by Mattias Persson (JIRA)

@itaiferber Are there any workarounds for this, except from the obvious "stop using enums as dictionary keys"?

@swift-ci
Copy link
Contributor Author

Comment by Clément Nonn (JIRA)

you can write you own implementation of conformance to Decodable / Encodable using a new type like `struct EnumCodableDictionary<Key, Value>`.

struct DecodingEnumArray<T: Decodable>: Decodable, ArrayConvertible, ExpressibleByArrayLiteral {
    typealias ArrayElementType = T
    typealias ArrayLiteralElement = T

    var values: [T]

    init(arrayLiteral elements: T...) {
        self.values = elements
    }

    init(from decoder: Decoder) throws {
        var container = try decoder.unkeyedContainer()

        guard let count = container.count else {
            throw DecodingError.dataCorruptedError(in: container, debugDescription: "count not known for this array")
        }

        var values = [T]()

        for _ in 0..<count {
            do {
                values.append(try container.decode(T.self))
            } catch {
                let error = DecodingError.dataCorruptedError(in: container, debugDescription: "entity not conformed. Aborted")
                   throw error
                
            }
        }
        self.values = values
    }
}

extension DecodingContinuableArray: Encodable where T: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.unkeyedContainer()

        try container.encode(contentsOf: self.values)
    }
}

This sample shows an array, but you can do the same for a dictionary.

@swift-ci
Copy link
Contributor Author

Comment by Mattias Persson (JIRA)

Thanks Zarghol (JIRA User)! Your code inspired me so I wrote this which suits my needs perfectly.
code found here
Update: The gist has been modified to use dynamicMemberLookup to transparently access any properties of the underlying Dictionary and it also modified to be able to wrap any Dictionary where the Key is a RawRepresentable. It is now nearly a drop in replacement for a Dictionary in the case of parsing JSON to Dictionary that has string representable enums as dictionary key.

@swift-ci
Copy link
Contributor Author

Comment by Clément Nonn (JIRA)

No problem 😉 just a little problem : in you init, when the enum value is not decoded, I think you should throw an error, not just continue silently. Else the result is not really conformed to the input..

@swift-ci
Copy link
Contributor Author

Comment by Mattias Persson (JIRA)

Good point! The code is written in the "app layer" and I'm thinking that the network client (which is running this code) shall survive if the backend suddenly sends a JSON containing a newly defined key/case. It's a bit of a project implementation requirement but you do have a valid point in the generic case.

@jarrodldavis
Copy link

Hey Zarghol (JIRA User) and snoozemoose (JIRA User),

I've come up with an alternative solution using the new Swift 5.1 @propertyWrapper.

This allows you to use the dictionary more directly without having to manually deal with forwarding function calls and member lookups.

@swift-ci
Copy link
Contributor Author

swift-ci commented Oct 2, 2019

Comment by Mattias Persson (JIRA)

@jarrodldavis Your solution works really good, thanks for this. I had to do a variant to support optional dictionaries too but other than that this is the cleanest solution yet.

@YOCKOW
Copy link
Collaborator

YOCKOW commented Jun 16, 2020

This should be also applicable to LosslessStringConvertible...

@YOCKOW
Copy link
Collaborator

YOCKOW commented Jun 25, 2020

Yet another workaround for this issue: https://github.com/YOCKOW/SwiftCodableDictionary

I realized that Key does not have to conform to Codable but should conform to CodingKey if we want to let Dictionary be en/decoded as expected. (Code)
(In other words, implementing this in the standard library will break the API.)

@swift-ci
Copy link
Contributor Author

Comment by Pheki (JIRA)

Note that this not only happens with Enums, but with other integer primitives, such as UInt8 and Int32, which otherwise act as Int for Codable. This is annoying as I need to implement Codable by hand for each of those structs or use the propertyWrapper dictionary instead...

@swift-ci swift-ci transferred this issue from apple/swift-issues Apr 25, 2022
@shahmishal shahmishal transferred this issue from apple/swift May 5, 2022
knovichikhin added a commit to knovichikhin/service-model-swift-code-generate that referenced this issue Aug 23, 2022
Maps with enum keys encode into an array of alternating keys and values, instead of a dictionary.
And vice versa, JSON dictionary fails to be decoded into such a map, instead expects an array.
See apple/swift-corelibs-foundation#3690
knovichikhin added a commit to amzn/service-model-swift-code-generate that referenced this issue Aug 23, 2022
Maps with enum keys encode into an array of alternating keys and values, instead of a dictionary.
And vice versa, JSON dictionary fails to be decoded into such a map, instead expects an array.
See apple/swift-corelibs-foundation#3690
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants