1

For a given JSON like below:

{
    "store": {
        "animals": [
            {
                "type": "dog"
            },
            {
                "type": "cat"
            }
        ]
    }
}

I can parse it with enum for type like following:

final class AnimalStore: Decodable {
    let store: Store
}

extension AnimalStore {
    struct Store: Decodable {
        let animals: [Animal]
    }
}

extension AnimalStore.Store {
    struct Animal: Decodable {
        let type: AnimalType?
    }
}

extension AnimalStore.Store.Animal {
    enum AnimalType: String, Decodable {
        case dog = "dog"
        case cat = "cat"
        //case unknown = how such a case could be implemented?
    }
}

And also since it is optional; it would work fine if type key value pair would be missing from json.

But I would like to have another case, lets call it unknown so that if any given type is not dog or cat (string being something else),type would be initialised as unknown. Right now it crashes if a type, other than dog or cat is given.

How initialising with another type other than the ones given can be implemented with enum?

In other words, for a given type like: "type": "bird" I would like type to be initialised as unknown.

emrepun
  • 2,496
  • 2
  • 15
  • 33
  • 1
    I searched for such a question but couldn't find it, thanks for pointing out duplication @LeoDabus – emrepun Oct 09 '19 at 12:25

3 Answers3

12

Add the enum case with a string, you may as well use "unknown".

To convert non-matching strings to unknowns, you have to manually implement init(from decoder: Decoder) at some point, either in your Animal or in AnimalType. I'd favour using AnimalType so that you don't have to manually decode any of the other properties of Animal.

enum AnimalType: String, Decodable {
    case dog = "dog"
    case cat = "cat"
    case unknown = "unknown"

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let string = try container.decode(String.self)
        self = AnimalType(rawValue: string) ?? .unknown
    }
}

If you did it in Animal, you'd need something like:

// Decode everything else...
type = try? decoder.decode(AnimalType.self, forKey: .type) ?? .unknown
jrturton
  • 118,105
  • 32
  • 252
  • 268
2

If you want to allow some alternative to your enum values you may use something like this:

enum Alt<S, A> {
    case standard(S)
    case alternative(A)
}

extension Alt: Decodable where S: Decodable, A: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let standard = try? container.decode(S.self) {
            self = .standard(standard)
        } else if let alternative = try? container.decode(A.self) {
            self = .alternative(alternative)
        } else {
            throw DecodingError.typeMismatch(
                Self.self,
                DecodingError.Context(codingPath: container.codingPath, debugDescription: "")
            )
        }
    }
}

And then change AnimalStore.Store.Animal declaration to this:

extension AnimalStore.Store {
    struct Animal: Decodable {
        let type: Alt<AnimalType, String>?
    }
}

Now it will try to decode it as AnimalType first and then, if fails will decode it as alternative type. So you can keep the value of the strings not in your enum.


EDIT: Or in situations when alternative is RawValue of standard you may use something like this:

enum RawBox<T>: RawRepresentable where T: RawRepresentable {
    typealias RawValue = T.RawValue

    case packed(T)
    case raw(RawValue)

    init(rawValue: Self.RawValue) {
        if let packed = T(rawValue: rawValue) {
            self = .packed(packed)
        } else {
            self = .raw(rawValue)
        }
    }

    var rawValue: T.RawValue {
        switch self {
        case .packed(let packed):
            return packed.rawValue
        case .raw(let raw):
            return raw
        }
    }
}

extension RawBox: Decodable where RawValue: Decodable {
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let raw = try container.decode(RawValue.self)
        self.init(rawValue: raw)
    }
}

extension RawBox: Encodable where RawValue: Encodable {
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(self.rawValue)
    }
}

extension AnimalStore.Store {
    struct Animal: Decodable {
        let type: RawBox<AnimalType>?
    }
}
user28434'mstep
  • 6,290
  • 2
  • 20
  • 35
1

I think you can try with this

extension AnimalStore.Store {
  struct Animal: Decodable {
    let type: AnimalType?

    enum CodingKeys: String, CodingKey {
      case type
    }

    init(from decoder: Decoder) throws {
      let values = try decoder.container(keyedBy: CodingKeys.self)
      type = try? values.decode(AnimalType.self, forKey: .type) ?? .unknown
    }
  }
}

extension AnimalStore.Store.Animal {
  enum AnimalType: String {
    case dog
    case cat
    case unknown
  }
}