Search code examples
jsonswiftenumsdecodable

Swift - How to use CodingKeys to map an Int to a custom enum?


I am loading in JSON which has integers that I wish to map to an enum

//Some JSON object
{
    "id": "....",
    "name": "Some Locomotive"
    "livery": 1,
    "generation": 1
    // other variables
}

I can load this JSON in using:

struct Locomotive: Codable {
    var id, name: String
    var generation: Int   
    // var livery: Int -- Replace this with my own enum (below)
    var livery: Livery?

    private enum CodingKeys: CodingKey {
        case id, name, generation
        case livery = "livery" // complains of raw value issue
    }
}

Currently both the generaiton and livery are just Integers; but to make coding easier for me I wish to use map the livery Integer to an enum; so rather than remembering 1 = green, etc; I can just say .green

But I am have trouble mapping the key livery to my enum.

Enum case cannot have a raw value if the enum does not have a raw type

But I'm sure it does; I've defined the raw value in the enum as private though;

enum Livery : Codable {
    case green, red, blue, yellow
}

extension Livery {

    private enum RawValue: Int, Codable, CaseIterable {
        case green = 1, red, yellow, blue
    }

    private enum CodingKeys: Int, CodingKey {
        case green, red, blue, yellow
    }

init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        let key = container.allKeys.first

        switch key {
        case .green:
            self = .green
        case .red:
            self = .red
        case .yellow:
            self = .yellow
        case .blue:
            self = .blue

        default:
            throw DecodingError.dataCorrupted(
                DecodingError.Context(
                    codingPath: container.codingPath,
                    debugDescription: "Error -- Unabled to decode."
                )
            )
        }
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()

        switch self {
        case .green:
            try container.encode(RawValue.green)
        case .red:
            try container.encode(RawValue.red)
        case .yellow:
            try container.encode(RawValue.yellow)
        case .blue:
            try container.encode(RawValue.blue)
        }

    }
}

The above enum decodes the raw value and encodes it.

However, I am unable to map the Livery in the parent struct to this enum and I'm wondering how I can do this?

...

I think I have to implement init(from decoder: Decoder) and encode(to encoder: Encoder) to this structure too -- especially if I want to save my data to JSON in the future; but I am not sure.

Thus, my query is - how do you map an Integer provided by the JSON to a custom enum for both saving (encoding) and loading (decoding).

With thanks


Solution

  • You can simplify your code quite a lot, all you need is the below code. By saying that your enum is of type Int swift can synthesise the correct decoding code

    struct Locomotive: Codable {
        var id, name: String
        var generation: Int
        var livery: Livery?
    }
    
    enum Livery: Int, Codable {
        case green = 1, red, blue, yellow
    }