Decodable, doesn't decode optional enum with invalid value
Asked Answered
F

4

12

I have defined a enum like this:

enum ClubLevel: Int, Codable {
    case golden = 1, silver, bronze
}

in my struct I have an optional property of type ClubLevel and when I decode this property in init(from decoder: Decoder):

self.clubLevel = try container.decode(ClubLevel?.self, forKey: .clubLevel)

I face this error:

debugDescription: "Cannot initialize ClubLevel from invalid Int value 0", underlyingError: nil"

I'm wondering even this property is optional, decoder won't continue

any idea?

Fondea answered 20/12, 2017 at 12:31 Comment(1)
thanks for your comment @hamish can you explain whats difference between try? container.decode(ClubLevel.self, forKey: .clubLevel) and try container.decode(ClubLevel?.self, forKey: .clubLevel). you can post your comment as an answerFondea
N
16

The line

self.clubLevel = try container.decode(ClubLevel?.self, forKey: .clubLevel)

doesn't try to decode ClubLevel, assigning nil if unsuccessful. What it does is:

  1. Try to decode nil (represented in JSON as null) for the clubLevel key. If unsuccessful,
  2. Try to decode a ClubLevel for the clubLevel key. If unsuccessful,
  3. Throw an error

So if the value for the clubLevel key is neither nil nor a valid ClubLevel representation, you'll get an error thrown. You'll note that this also means you'll get an error thrown if the clubLevel key is missing entirely (rather than being present with a value of nil).

Ignoring missing keys is done with decodeIfPresent:

self.clubLevel = try container.decodeIfPresent(ClubLevel.self, forKey: .clubLevel)

This will now:

  1. Return nil if the clubLevel key is missing from the container. If they key is present,
  2. Try to decode nil (represented in JSON as null) for the clubLevel key. If unsuccessful,
  3. Try to decode a ClubLevel for the clubLevel key. If unsuccessful,
  4. Throw an error

This is the default behaviour for decoding optionals in a compiler-generated implementation of init(from:). It will still throw an error in your case as the value for the clubLevel key is not a valid ClubLevel.

If you want to just try and decode a ClubLevel, assigning nil on the decoding failing for any reason (key missing, invalid value, etc.), then you want to use try?:

self.clubLevel = try? container.decode(ClubLevel.self, forKey: .clubLevel)
Natiha answered 24/12, 2017 at 13:21 Comment(3)
What if we need to assign a default value on ANY failing reason?Recliner
@Recliner You’d want to use the nil coalescing operator, e.g (try? container.decode(ClubLevel.self, forKey: .clubLevel)) ?? defaultNatiha
Ahh, I was missing the parenthesis. Thanks!Recliner
D
4

I came across the same problem and thought I'd add my solution for anyone interested.

The idea is to wrap the enum inside the following struct:

struct OptionalDecodableEnum<T>: Decodable where T: RawRepresentable, T.RawValue: Decodable {
    let value: T?

    init(from decoder: Decoder) throws {
        value = T(rawValue: try decoder.singleValueContainer().decode(T.RawValue.self))
    }
}

The main benefit is that you don't have to implement Decodable every time you want an optional enum. You also don't need the optional chaining with extra parentheses.

However, you will have to return the inner value property when you want to use it. E.g.

struct Foo: Decodable {
    enum ClubLevel: Int, Codable {
        case golden = 1, silver, bronze
    }

    let clubLevel: OptionalDecodableEnum<ClubLevel>
}

let foo = try jsonDecoder.decode(Foo.self, from: data)
print(String(describing: foo.clubLevel.value))
Dishearten answered 8/8, 2018 at 16:50 Comment(0)
B
1

I was able to convert Guy Kogus' answer into a property wrapper, which removes some of the friction when using this code.

@propertyWrapper struct OptionalCodableEnum<T>: Codable where T: RawRepresentable, T.RawValue: Codable {
    
    var wrappedValue: T?
    
    init(wrappedValue: T?) {
        self.wrappedValue = wrappedValue
    }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let rawValue = try? container.decode(T.RawValue.self),
           let result = T(rawValue: rawValue) {
            wrappedValue = result
        }
    }
    
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        try container.encode(wrappedValue?.rawValue)
    }
}

Usage looks like this:

    struct MyCodable: Codable {
        enum MyCodableEnum: Int, Codable {
            case first = 0, second
        }
        
        @OptionalCodableEnum
        var anEnumValue: MyCodableEnum?
    }
Biogen answered 12/4 at 3:46 Comment(0)
S
0

I was looking for a method which solves a similar problem as described in this post - just for an array of enum values instead of a single one. Since this was the first post showing up when looking for an answer to this problem I wanted to share my solution here in case it could maybe help someone with a similar problem :)

Problematic Example:

{
  "exampleArray": [
     "FirstExample",
     "SecondExample",
     "abcde123",
     "FourthExample"
  ]
}
// ...
// Usage
init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    enumArray = try? container.decodeEnumArray([EnumType].self, forKey: .enumArray)
}

// ...

extension KeyedDecodingContainer {

    func decodeEnumArray<T: RawRepresentable>(_: [T].Type, forKey key: Self.Key) throws -> [T] where T.RawValue: Decodable {
        return try decode([T.RawValue].self, forKey: key).map { T(rawValue: $0) }.compactMap { $0 }
    }
}

enumArray will then be [FirstExample, SecondExample, FourthExample]

Spangler answered 13/5, 2020 at 20:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.