J'ai défini un enum
comme suit:
enum Type: String, Codable {
case text = "text"
case image = "image"
case document = "document"
case profile = "profile"
case sign = "sign"
case inputDate = "input_date"
case inputText = "input_text"
case inputNumber = "input_number"
case inputOption = "input_option"
case unknown
}
qui mappe une propriété de chaîne JSON. La sérialisation et la désérialisation automatiques fonctionnent bien, mais j'ai trouvé que si une chaîne différente est rencontrée, la désérialisation échoue.
Est-il possible de définir un cas unknown
qui mappe tout autre cas disponible?
Cela peut être très utile, car ces données proviennent d'une API RESTFul qui, peut-être, peut changer à l'avenir.
Vous pouvez étendre votre Codable
Type et attribuer une valeur par défaut en cas d'échec:
enum Type: String {
case text,
image,
document,
profile,
sign,
inputDate = "input_date",
inputText = "input_text" ,
inputNumber = "input_number",
inputOption = "input_option",
unknown
}
extension Type: Codable {
public init(from decoder: Decoder) throws {
self = try Type(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}
éditer/mettre à jour:
Swift 5 ou version ultérieure nous pouvons créer un protocole qui utilise par défaut le dernier cas d'une énumération de chaîne CaseIterable
:
protocol CaseIterableDefaultsLast: Codable & CaseIterable & RawRepresentable
where Self.RawValue == String, Self.AllCases: BidirectionalCollection { }
extension CaseIterableDefaultsLast {
init(from decoder: Decoder) throws {
self = try Self(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? Self.allCases.last!
}
}
Test de terrain de jeu:
enum Type: String, CaseIterableDefaultsLast {
case text, image, document, profile, sign, inputDate = "input_date", inputText = "input_text" , inputNumber = "input_number", inputOption = "input_option", unknown
}
let types = try! JSONDecoder().decode([Type].self , from: Data(#"["text","image","sound"]"#.utf8)) // [text, image, unknown]
Vous pouvez supprimer le type brut de votre Type
et rendre la casse inconnue qui gère la valeur associée. Mais cela a un coût. Vous avez en quelque sorte besoin des valeurs brutes pour vos cas. Inspiré des réponses this et this SO), j'ai trouvé cette élégante solution à votre problème.
Pour pouvoir stocker les valeurs brutes , nous allons maintenir une autre énumération, mais comme privée:
enum Type {
case text
case image
case document
case profile
case sign
case inputDate
case inputText
case inputNumber
case inputOption
case unknown(String)
// Make this private
private enum RawValues: String, Codable {
case text = "text"
case image = "image"
case document = "document"
case profile = "profile"
case sign = "sign"
case inputDate = "input_date"
case inputText = "input_text"
case inputNumber = "input_number"
case inputOption = "input_option"
// No such case here for the unknowns
}
}
Déplacez la partie encoding
& decoding
vers les extensions:
extension Type: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// As you already know your RawValues is String actually, you decode String here
let stringForRawValues = try container.decode(String.self)
// This is the trick here...
switch stringForRawValues {
// Now You can switch over this String with cases from RawValues since it is String
case RawValues.text.rawValue:
self = .text
case RawValues.image.rawValue:
self = .image
case RawValues.document.rawValue:
self = .document
case RawValues.profile.rawValue:
self = .profile
case RawValues.sign.rawValue:
self = .sign
case RawValues.inputDate.rawValue:
self = .inputDate
case RawValues.inputText.rawValue:
self = .inputText
case RawValues.inputNumber.rawValue:
self = .inputNumber
case RawValues.inputOption.rawValue:
self = .inputOption
// Now handle all unknown types. You just pass the String to Type's unknown case.
// And this is true for every other unknowns that aren't defined in your RawValues
default:
self = .unknown(stringForRawValues)
}
}
}
extension Type: Encodable {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .text:
try container.encode(RawValues.text)
case .image:
try container.encode(RawValues.image)
case .document:
try container.encode(RawValues.document)
case .profile:
try container.encode(RawValues.profile)
case .sign:
try container.encode(RawValues.sign)
case .inputDate:
try container.encode(RawValues.inputDate)
case .inputText:
try container.encode(RawValues.inputText)
case .inputNumber:
try container.encode(RawValues.inputNumber)
case .inputOption:
try container.encode(RawValues.inputOption)
case .unknown(let string):
// You get the actual String here from the associated value and just encode it
try container.encode(string)
}
}
}
Je viens de l'envelopper dans une structure de conteneur (car nous utiliserons JSONEncoder/JSONDecoder) comme:
struct Root: Codable {
let type: Type
}
Pour les valeurs autres que le cas inconnu:
let rootObject = Root(type: Type.document)
do {
let encodedRoot = try JSONEncoder().encode(rootObject)
do {
let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
print(decodedRoot.type) // document
} catch {
print(error)
}
} catch {
print(error)
}
Pour les valeurs avec cas inconnu:
let rootObject = Root(type: Type.unknown("new type"))
do {
let encodedRoot = try JSONEncoder().encode(rootObject)
do {
let decodedRoot = try JSONDecoder().decode(Root.self, from: encodedRoot)
print(decodedRoot.type) // unknown("new type")
} catch {
print(error)
}
} catch {
print(error)
}
Je mets l'exemple avec des objets locaux. Vous pouvez essayer avec votre réponse API REST.
Voici une alternative basée sur la réponse de nayem qui offre une syntaxe légèrement plus rationalisée en utilisant la liaison facultative de l'initialisation interne de RawValues
:
enum MyEnum: Codable {
case a, b, c
case other(name: String)
private enum RawValue: String, Codable {
case a = "a"
case b = "b"
case c = "c"
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
if let value = RawValue(rawValue: decodedString) {
switch value {
case .a:
self = .a
case .b:
self = .b
case .c:
self = .c
}
} else {
self = .other(name: decodedString)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a:
try container.encode(RawValue.a)
case .b:
try container.encode(RawValue.b)
case .c:
try container.encode(RawValue.c)
case .other(let name):
try container.encode(name)
}
}
}
Si vous êtes certain que tous vos noms de cas énumération existants correspondent aux valeurs de chaîne sous-jacentes qu'ils représentent, vous pouvez rationaliser RawValue
pour:
private enum RawValue: String, Codable {
case a, b, c
}
... et encode(to:)
pour:
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let rawValue = RawValue(rawValue: String(describing: self)) {
try container.encode(rawValue)
} else if case .other(let name) = self {
try container.encode(name)
}
}
Voici un exemple pratique d'utilisation, par exemple, vous voulez modéliser SomeValue
qui a une propriété que vous souhaitez modéliser comme une énumération:
struct SomeValue: Codable {
enum MyEnum: Codable {
case a, b, c
case other(name: String)
private enum RawValue: String, Codable {
case a = "a"
case b = "b"
case c = "letter_c"
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
if let value = RawValue(rawValue: decodedString) {
switch value {
case .a:
self = .a
case .b:
self = .b
case .c:
self = .c
}
} else {
self = .other(name: decodedString)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .a:
try container.encode(RawValue.a)
case .b:
try container.encode(RawValue.b)
case .c:
try container.encode(RawValue.c)
case .other(let name):
try container.encode(name)
}
}
}
}
let jsonData = """
[
{ "value": "a" },
{ "value": "letter_c" },
{ "value": "c" },
{ "value": "Other value" }
]
""".data(using: .utf8)!
let decoder = JSONDecoder()
if let values = try? decoder.decode([SomeValue].self, from: jsonData) {
values.forEach { print($0.value) }
let encoder = JSONEncoder()
if let encodedJson = try? encoder.encode(values) {
print(String(data: encodedJson, encoding: .utf8)!)
}
}
/* Prints:
a
c
other(name: "c")
other(name: "Other value")
[{"value":"a"},{"value":"letter_c"},{"value":"c"},{"value":"Other value"}]
*/
Ajoutez cette extension et définissez YourEnumName
.
extension <#YourEnumName#>: Codable {
public init(from decoder: Decoder) throws {
self = try <#YourEnumName#>(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .unknown
}
}
Vous devez implémenter l'initialiseur init(from decoder: Decoder) throws
et vérifier une valeur valide:
struct SomeStruct: Codable {
enum SomeType: String, Codable {
case text
case image
case document
case profile
case sign
case inputDate = "input_date"
case inputText = "input_text"
case inputNumber = "input_number"
case inputOption = "input_option"
case unknown
}
var someType: SomeType
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
someType = (try? values.decode(SomeType.self, forKey: .someType)) ?? .unknown
}
}
@LeoDabus merci pour vos réponses. Je les ai légèrement modifiés pour créer un protocole pour les énumérations de chaînes qui semble fonctionner pour moi:
protocol CodableWithUnknown: Codable {}
extension CodableWithUnknown where Self: RawRepresentable, Self.RawValue == String {
init(from decoder: Decoder) throws {
do {
try self = Self(rawValue: decoder.singleValueContainer().decode(RawValue.self))!
} catch {
if let unknown = Self(rawValue: "unknown") {
self = unknown
} else {
throw error
}
}
}
}