Skip to content

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

Open

Description

@swift-ci
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 ]

Activity

  1. swift-ci commented on May 27, 2018

    @swift-ci
    ContributorAuthor

    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

  2. itaiferber commented on May 28, 2018

    @itaiferber
    Contributor

    @swift-ci create

  3. swift-ci commented on Apr 26, 2019

    @swift-ci
    ContributorAuthor

    Comment by Mattias Persson (JIRA)

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

  4. swift-ci commented on Sep 13, 2019

    @swift-ci
    ContributorAuthor

    Comment by Mattias Persson (JIRA)

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

  5. swift-ci commented on Sep 16, 2019

    @swift-ci
    ContributorAuthor

    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.

  6. swift-ci commented on Sep 17, 2019

    @swift-ci
    ContributorAuthor

    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.

  7. swift-ci commented on Sep 17, 2019

    @swift-ci
    ContributorAuthor

    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..

  8. swift-ci commented on Sep 17, 2019

    @swift-ci
    ContributorAuthor

    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.

  9. jarrodldavis commented on Sep 26, 2019

    @jarrodldavis

    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.

  10. swift-ci commented on Oct 2, 2019

    @swift-ci
    ContributorAuthor

    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.

  11. YOCKOW commented on Jun 16, 2020

    @YOCKOW
    Member

    This should be also applicable to LosslessStringConvertible...

  12. YOCKOW commented on Jun 25, 2020

    @YOCKOW
    Member

    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.)

  13. swift-ci commented on Aug 24, 2020

    @swift-ci
    ContributorAuthor

    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...

  14. transferred this issue fromswiftlang/swifton May 5, 2022
  15. maxches99 commented on Aug 10, 2026

    @maxches99

    This was addressed by SE-0320 (CodingKeyRepresentable, Swift 5.6), which cites this very report as its motivation. Conforming the key type is enough — RawRepresentable types with a String/Int raw value get the default implementation, so no custom wrapper is needed anymore:

    enum Key: String, Codable, CodingKeyRepresentable {
        case key
    }
    
    try JSONEncoder().encode([Key.key: "value"])
    // {"key":"value"}  — and it round-trips back through JSONDecoder

    As @YOCKOW noted earlier in the thread, changing the unconditional behavior in the stdlib would be source- and format-breaking, which is why the opt-in conformance is the supported answer rather than a fix to Dictionary's Encodable implementation (whose documented contract is: keyed container for String/Int/CodingKeyRepresentable keys, alternating key/value pairs otherwise).

    I believe this can be closed as resolved by SE-0320. Same for the swift-foundation copy of this report, swiftlang/swift-foundation#1742.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions