Skip to content

Instantly share code, notes, and snippets.

@christianselig
Last active October 27, 2022 18:55
Show Gist options
  • Save christianselig/92b75f6bc94a76b8ab2a45b0115b39c2 to your computer and use it in GitHub Desktop.
Save christianselig/92b75f6bc94a76b8ab2a45b0115b39c2 to your computer and use it in GitHub Desktop.

If I have a JSON Codable object in Swift that looks like:

struct IceCreamStore: Codable {
    let iceCreams: [IceCream: Int]
}

enum IceCream: String, Codable {
    case chocolate, vanilla
}

And I accidentally encoded a strawberry field from a new version of the app, so that the dictionary now has a strawberry field in it, that this older version of the app doesn't know how to deal with (and thus can't decode it and errors), is there a way to conditionally decode it and just ignore that strawberry value? I tried using CodingKeys and decoding it as a [String: IceCreamInfo] instead manually, but no dice, still won't decode as that. I don't want to add strawberry manually as a value for a number of reasons but just consider those academic.

It's basically trying to ingest:

{
    "chocolate": 8,
    "vanilla": 4,
    "strawberry": 3
}

When it doesn't know how to deal with strawberry, and I want to just have it ignore the strawberry.

Like, in my head I want to do:

let container = try decoder.container(keyedBy: CodingKeys.self)
let manualDictionary = try container.decode([String: Int].self, forKey: .iceCreams)

var realDictionary: [IceCream: Int] = [:]

for key, value in manualDictionary {
    guard let iceCream = IceCream(rawValue: key) else { continue }
    realDictionary[iceCream] = value
}

self.iceCreams = realDictionary

But that's not working (it won't let me decode it as a [String: Int]).

@jegnux
Copy link

jegnux commented Oct 27, 2022

The issue is not really the decode but the encode. Encodable doesn't encode [KeyType: ValueType] as { "key" : value } even if KeyType is RawRepresentable where RawValue == String. If you read the json made out from your encode function, you'll see you really get an array alternating keys and values:

enum IceCream: String, Codable {
    case chocolate, vanilla, strawberry
}

let store = IceCreamStore(iceCreams: [
    .chocolate: 8,
    .vanilla: 9,
    .strawberry: 10
])

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
    
let data = try! encoder.encode(store)
print(String(data: data, encoding: .utf8)!)
{
  "iceCreams" : [
    "vanilla",
    9,
    "strawberry",
    10,
    "chocolate",
    8
  ]
}

So the idea is to have a custom init(from decoder: Decoder) AND a custom encode(to encoder: Encoder):

struct IceCreamStore {
    let iceCreams: [IceCream: Int]
}

extension IceCreamStore: Codable {
    enum CodingKeys: String, CodingKey {
        case iceCreams
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        
        iceCreams = Dictionary(
            uniqueKeysWithValues: try container
            .decode([String: Int].self, forKey: .iceCreams)
            .compactMap { key, value in 
                guard let iceCream = IceCream(rawValue: key) else { return nil }
                return (iceCream, value)
            }
        )
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(Dictionary(
            uniqueKeysWithValues: iceCreams.map { iceCream, value in 
                (iceCream.rawValue, value)
            }
        ), forKey: .iceCreams)
    }
}

Which have the correct, expected, output:

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
    
let data = try! encoder.encode(store)
print(String(data: data, encoding: .utf8)!)
{
  "iceCreams" : {
    "chocolate" : 8,
    "vanilla" : 9,
    "strawberry" : 10
  }
}

Edit: on Swift 5.6, you don't need the custom encode(to encoder: Encoder). You can simply add CodingKeyRepresentable conformance on IceCream (and keep the custom init(from decoder: Decoder) though):

enum IceCream: String, Codable, CodingKeyRepresentable {
    case chocolate, vanilla, strawberry
}

see SE-0320 Allow coding of non String / Int keyed Dictionary into a KeyedContainer

@christianselig
Copy link
Author

Thanks y'all @jegnux @eliyap @damir @quinwoods this helped a ton :)

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