Comment rendre une énumération décodable dans Swift 4?


157
enum PostType: Decodable {

    init(from decoder: Decoder) throws {

        // What do i put here?
    }

    case Image
    enum CodingKeys: String, CodingKey {
        case image
    }
}

Que dois-je mettre pour compléter cela? Aussi, disons que j'ai changé le caseen ceci:

case image(value: Int)

Comment rendre cela conforme à Decodable?

EDit Voici mon code complet (qui ne fonctionne pas)

let jsonData = """
{
    "count": 4
}
""".data(using: .utf8)!

        do {
            let decoder = JSONDecoder()
            let response = try decoder.decode(PostType.self, from: jsonData)

            print(response)
        } catch {
            print(error)
        }
    }
}

enum PostType: Int, Codable {
    case count = 4
}

Final Edit Aussi, comment va-t-il gérer une énumération comme celle-ci?

enum PostType: Decodable {
    case count(number: Int)
}

Réponses:


262

Il est assez facile, il suffit d' utiliser Stringou les Intvaleurs brutes qui sont implicitement attribuées.

enum PostType: Int, Codable {
    case image, blob
}

imageest encodé vers 0et blobvers1

Ou

enum PostType: String, Codable {
    case image, blob
}

imageest encodé vers "image"et blobvers"blob"


Voici un exemple simple de son utilisation:

enum PostType : Int, Codable {
    case count = 4
}

struct Post : Codable {
    var type : PostType
}

let jsonString = "{\"type\": 4}"

let jsonData = Data(jsonString.utf8)

do {
    let decoded = try JSONDecoder().decode(Post.self, from: jsonData)
    print("decoded:", decoded.type)
} catch {
    print(error)
}

1
J'ai essayé le code que vous avez suggéré, mais cela ne fonctionne pas. J'ai modifié mon code pour afficher le JSON que j'essaie de décoder
Swift Nub

8
Une énumération ne peut pas être encodée / décodée uniquement. Il doit être intégré dans une structure. J'ai ajouté un exemple.
vadian

Je marquerai ceci comme correct. Mais il y avait une dernière partie dans la question ci-dessus qui n'a pas été répondue. Et si mon énumération ressemblait à ceci? (modifié ci-dessus)
swift nub

Si vous utilisez des énumérations avec des types associés, vous devez écrire des méthodes de codage et de décodage personnalisées. S'il vous plaît lire Encodage et décodage de types personnalisés
vadian

1
À propos de "Une énumération ne peut pas être encodée / décodée uniquement.", Il semble être résolu à iOS 13.3. Je teste iOS 13.3et iOS 12.4.3, ils se comportent différemment. Sous iOS 13.3, enum peut être encodé / décodé uniquement.
AechoLiu

111

Comment rendre les énumérations avec les types associés conformes à Codable

Cette réponse est similaire à celle de @Howard Lovatt mais évite de créer une PostTypeCodableFormstructure et utilise à la place le KeyedEncodingContainertype fourni par Apple comme propriété sur Encoderet Decoder, ce qui réduit le passe-partout.

enum PostType: Codable {
    case count(number: Int)
    case title(String)
}

extension PostType {

    private enum CodingKeys: String, CodingKey {
        case count
        case title
    }

    enum PostTypeCodingError: Error {
        case decoding(String)
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        if let value = try? values.decode(Int.self, forKey: .count) {
            self = .count(number: value)
            return
        }
        if let value = try? values.decode(String.self, forKey: .title) {
            self = .title(value)
            return
        }
        throw PostTypeCodingError.decoding("Whoops! \(dump(values))")
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        switch self {
        case .count(let number):
            try container.encode(number, forKey: .count)
        case .title(let value):
            try container.encode(value, forKey: .title)
        }
    }
}

Ce code fonctionne pour moi sur Xcode 9b3.

import Foundation // Needed for JSONEncoder/JSONDecoder

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let decoder = JSONDecoder()

let count = PostType.count(number: 42)
let countData = try encoder.encode(count)
let countJSON = String.init(data: countData, encoding: .utf8)!
print(countJSON)
//    {
//      "count" : 42
//    }

let decodedCount = try decoder.decode(PostType.self, from: countData)

let title = PostType.title("Hello, World!")
let titleData = try encoder.encode(title)
let titleJSON = String.init(data: titleData, encoding: .utf8)!
print(titleJSON)
//    {
//        "title": "Hello, World!"
//    }
let decodedTitle = try decoder.decode(PostType.self, from: titleData)

J'adore cette réponse! À titre de note, cet exemple est également repris dans un article sur objc.io sur la création de Eithercodables
Ben Leggiero

La meilleure réponse
Peter Suwara

38

Swift lèverait une .dataCorruptederreur s'il rencontre une valeur d'énumération inconnue. Si vos données proviennent d'un serveur, il peut vous envoyer une valeur d'énumération inconnue à tout moment (bogue côté serveur, nouveau type ajouté dans une version d'API et vous voulez que les versions précédentes de votre application gèrent le cas avec élégance, etc.), vous feriez mieux d'être préparé et de coder "style défensif" pour décoder en toute sécurité vos énumérations.

Voici un exemple de marche à suivre, avec ou sans valeur associée

    enum MediaType: Decodable {
       case audio
       case multipleChoice
       case other
       // case other(String) -> we could also parametrise the enum like that

       init(from decoder: Decoder) throws {
          let label = try decoder.singleValueContainer().decode(String.self)
          switch label {
             case "AUDIO": self = .audio
             case "MULTIPLE_CHOICES": self = .multipleChoice
             default: self = .other
             // default: self = .other(label)
          }
       }
    }

Et comment l'utiliser dans une structure englobante:

    struct Question {
       [...]
       let type: MediaType

       enum CodingKeys: String, CodingKey {
          [...]
          case type = "type"
       }


   extension Question: Decodable {
      init(from decoder: Decoder) throws {
         let container = try decoder.container(keyedBy: CodingKeys.self)
         [...]
         type = try container.decode(MediaType.self, forKey: .type)
      }
   }

1
Merci, votre réponse est beaucoup plus facile à comprendre.
DazChong

1
Cette réponse m'a aidé aussi, merci. Il peut être amélioré en faisant hériter votre enum de String, vous n'avez pas besoin de basculer les chaînes
Gobe

27

Pour étendre la réponse de @ Toka, vous pouvez également ajouter une valeur brute représentable à l'énumération et utiliser le constructeur facultatif par défaut pour construire l'énumération sans switch:

enum MediaType: String, Decodable {
  case audio = "AUDIO"
  case multipleChoice = "MULTIPLE_CHOICES"
  case other

  init(from decoder: Decoder) throws {
    let label = try decoder.singleValueContainer().decode(String.self)
    self = MediaType(rawValue: label) ?? .other
  }
}

Il peut être étendu à l'aide d'un protocole personnalisé qui permet de refactoriser le constructeur:

protocol EnumDecodable: RawRepresentable, Decodable {
  static var defaultDecoderValue: Self { get }
}

extension EnumDecodable where RawValue: Decodable {
  init(from decoder: Decoder) throws {
    let value = try decoder.singleValueContainer().decode(RawValue.self)
    self = Self(rawValue: value) ?? Self.defaultDecoderValue
  }
}

enum MediaType: String, EnumDecodable {
  static let defaultDecoderValue: MediaType = .other

  case audio = "AUDIO"
  case multipleChoices = "MULTIPLE_CHOICES"
  case other
}

Il peut également être facilement étendu pour générer une erreur si une valeur d'énumération non valide a été spécifiée, plutôt que par défaut sur une valeur. Gist avec ce changement est disponible ici: https://gist.github.com/stephanecopin/4283175fabf6f0cdaf87fef2a00c8128 .
Le code a été compilé et testé avec Swift 4.1 / Xcode 9.3.


1
C'est la réponse que je cherchais.
Nathan Hosselton

7

Une variante de la réponse de @ proxpero qui est terser serait de formuler le décodeur comme:

public init(from decoder: Decoder) throws {
    let values = try decoder.container(keyedBy: CodingKeys.self)
    guard let key = values.allKeys.first else { throw err("No valid keys in: \(values)") }
    func dec<T: Decodable>() throws -> T { return try values.decode(T.self, forKey: key) }

    switch key {
    case .count: self = try .count(dec())
    case .title: self = try .title(dec())
    }
}

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    switch self {
    case .count(let x): try container.encode(x, forKey: .count)
    case .title(let x): try container.encode(x, forKey: .title)
    }
}

Cela permet au compilateur de vérifier de manière exhaustive les cas et ne supprime pas non plus le message d'erreur pour le cas où la valeur codée ne correspond pas à la valeur attendue de la clé.


Je suis d'accord que c'est mieux.
proxpero

6

En fait, les réponses ci-dessus sont vraiment excellentes, mais il leur manque quelques détails sur ce dont beaucoup de gens ont besoin dans un projet client / serveur développé en permanence. Nous développons une application pendant que notre backend évolue continuellement au fil du temps, ce qui signifie que certains cas d'énumération changeront cette évolution. Nous avons donc besoin d'une stratégie de décodage enum capable de décoder des tableaux d'énumérations contenant des cas inconnus. Sinon, le décodage de l'objet qui contient le tableau échoue tout simplement.

Ce que j'ai fait est assez simple:

enum Direction: String, Decodable {
    case north, south, east, west
}

struct DirectionList {
   let directions: [Direction]
}

extension DirectionList: Decodable {

    public init(from decoder: Decoder) throws {

        var container = try decoder.unkeyedContainer()

        var directions: [Direction] = []

        while !container.isAtEnd {

            // Here we just decode the string from the JSON which always works as long as the array element is a string
            let rawValue = try container.decode(String.self)

            guard let direction = Direction(rawValue: rawValue) else {
                // Unknown enum value found - ignore, print error to console or log error to analytics service so you'll always know that there are apps out which cannot decode enum cases!
                continue
            }
            // Add all known enum cases to the list of directions
            directions.append(direction)
        }
        self.directions = directions
    }
}

Bonus: Masquer la mise en œuvre> En faire une collection

Masquer les détails de l'implémentation est toujours une bonne idée. Pour cela, vous aurez besoin d'un peu plus de code. L'astuce est de se conformer DirectionsListà Collectionet rendre votre intérieur listtableau privé:

struct DirectionList {

    typealias ArrayType = [Direction]

    private let directions: ArrayType
}

extension DirectionList: Collection {

    typealias Index = ArrayType.Index
    typealias Element = ArrayType.Element

    // The upper and lower bounds of the collection, used in iterations
    var startIndex: Index { return directions.startIndex }
    var endIndex: Index { return directions.endIndex }

    // Required subscript, based on a dictionary index
    subscript(index: Index) -> Element {
        get { return directions[index] }
    }

    // Method that returns the next index when iterating
    func index(after i: Index) -> Index {
        return directions.index(after: i)
    }
}

Vous pouvez en savoir plus sur la conformité aux collections personnalisées dans cet article de blog de John Sundell: https://medium.com/@johnsundell/creating-custom-collections-in-swift-a344e25d0bb0


5

Vous pouvez faire ce que vous voulez, mais c'est un peu compliqué :(

import Foundation

enum PostType: Codable {
    case count(number: Int)
    case comment(text: String)

    init(from decoder: Decoder) throws {
        self = try PostTypeCodableForm(from: decoder).enumForm()
    }

    func encode(to encoder: Encoder) throws {
        try PostTypeCodableForm(self).encode(to: encoder)
    }
}

struct PostTypeCodableForm: Codable {
    // All fields must be optional!
    var countNumber: Int?
    var commentText: String?

    init(_ enumForm: PostType) {
        switch enumForm {
        case .count(let number):
            countNumber = number
        case .comment(let text):
            commentText = text
        }
    }

    func enumForm() throws -> PostType {
        if let number = countNumber {
            guard commentText == nil else {
                throw DecodeError.moreThanOneEnumCase
            }
            return .count(number: number)
        }
        if let text = commentText {
            guard countNumber == nil else {
                throw DecodeError.moreThanOneEnumCase
            }
            return .comment(text: text)
        }
        throw DecodeError.noRecognizedContent
    }

    enum DecodeError: Error {
        case noRecognizedContent
        case moreThanOneEnumCase
    }
}

let test = PostType.count(number: 3)
let data = try JSONEncoder().encode(test)
let string = String(data: data, encoding: .utf8)!
print(string) // {"countNumber":3}
let result = try JSONDecoder().decode(PostType.self, from: data)
print(result) // count(3)

hack intéressant
Roman Filippov
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.