You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
It's not uncommon for the back-end guys to return some JSON where the type of a field is either a number or a string.
This causes inconvenience at the app development level where it's not enough to declare vanilla Codable structs.
Thankfully, Decodable makes this way less messy than first imagined.
We can wrap the either logic in a separate Decodable type that handles this for us:
structProduct:Decodable{letid:EitherIntOrStringstructEitherIntOrString:Decodable{letvalue:Intinit(from decoder:Decoder)throws{letvalues=try decoder.singleValueContainer()do{
value =try values.decode(Int.self)}catch{letstring=try values.decode(String.self)
guard let int =Int(string)else{throwParsingError.stringParsingError
}
value = int
}}}enumParsingError:Error{case stringParsingError
}}
So, JSON like the following parses successfully:
[
{ "id": 12 },
{ "id": "14" }
]
Generalizing
We can make a generic either type out of this.
All we need is two Decodable types, and a converter from a type to the other.
Let's see:
protocolConverter{associatedtypeT1associatedtypeT2staticfunc convert(_ t2:T2)->T1?}structDecodableEither<T1:Decodable,T2:Decodable,C:Converter>:Decodablewhere C.T1 ==T1, C.T2 ==T2{letvalue:T1init(from decoder:Decoder)throws{letvalues=try decoder.singleValueContainer()do{
value =try values.decode(T1.self)}catch{lett2=try values.decode(T2.self)
guard let t1 =C.convert(t2)else{throwError.conversionError
}
value = t1
}}enumError:Swift.Error{case conversionError
}}
Let's break this down:
DecodableEither<T1: Decodable, T2: Decodable, C: Converter>: Decodable.
Here we declare a generic struct that conforms to Decodable, and depends on three types.
The first two are any Decodable types.
The third is just a type that conforms to a protocol called Converter that we will use for converting from type T2 to T1.
The Converter protocol declares a static function that converts from a generic type to another.
Such protocol is called protocol with associated types, commonly called "PATs".
Now, the Swiftiest thing in this code, the type constraints. where C.T1 == T1, C.T2 == T2.
This part after the DecodableEither declaration is what ensures type-safety and makes things work together.
Here we tell the Swift compiler to ensure that any Converter type passed to us here must have its two associated types be the very two types passed to the DecodableEither.
This what makes that line let t1 = C.convert(t2) in the alternate decoding phase work, and infer correctly the given types.
I stumbled upon this brilliant suggestion by Jussi Laitinen.
Now, our solution can be cleaner by eliminating the third Converter type, and instead requiring our first type to be convertible from the second type. Let's see this in code:
protocolConvertible{associatedtypeTinit?(_ value:T)}structDecodableEither<T1:Decodable&Convertible,T2:Decodable>:Decodablewhere T1.T ==T2{letvalue:T1init(from decoder:Decoder)throws{letvalues=try decoder.singleValueContainer()do{
value =try values.decode(T1.self)}catch{lett2=try values.decode(T2.self)
guard let t1 =T1(t2)else{throwError.conversionError
}
value = t1
}}enumError:Swift.Error{case conversionError
}}
Also, converting from String to Int is a lot simpler now, since Int already has a failable initializer that accepts a String. We just extend Int to conform to our Convertible protocol while stating that the generic/associated type T to be String.
extensionInt:Convertible{typealiasT=String}
Update (29-04-2020)
Fadi suggested a more generic solution to this problem that leaves the converting step to the user.
I like it. Here it is:
enumDecodableEither<T1:Decodable,T2:Decodable>:Decodable{case v1(T1)case v2(T2)init(from decoder:Decoder)throws{letcontainer=try decoder.singleValueContainer()
if let v1 =try? container.decode(T1.self){self=.v1(v1)}else{self=try.v2(container.decode(T2.self))}}varv1:T1?{
switch self{case.v1(let value):return value
default:returnnil}}varv2:T2?{
switch self{case.v2(let value):return value
default:returnnil}}}
The text was updated successfully, but these errors were encountered:
(Originally published 2019-10-4)
It's not uncommon for the back-end guys to return some JSON where the type of a field is either a number or a string.
This causes inconvenience at the app development level where it's not enough to declare vanilla
Codable
structs.Thankfully,
Decodable
makes this way less messy than first imagined.We can wrap the either logic in a separate
Decodable
type that handles this for us:So, JSON like the following parses successfully:
Generalizing
We can make a generic either type out of this.
All we need is two
Decodable
types, and a converter from a type to the other.Let's see:
Let's break this down:
DecodableEither<T1: Decodable, T2: Decodable, C: Converter>: Decodable
.Here we declare a generic struct that conforms to
Decodable
, and depends on three types.The first two are any
Decodable
types.The third is just a type that conforms to a protocol called
Converter
that we will use for converting from typeT2
toT1
.Converter
protocol declares a static function that converts from a generic type to another.Such protocol is called protocol with associated types, commonly called "PATs".
where C.T1 == T1, C.T2 == T2
.This part after the
DecodableEither
declaration is what ensures type-safety and makes things work together.Here we tell the Swift compiler to ensure that any
Converter
type passed to us here must have its two associated types be the very two types passed to theDecodableEither
.This what makes that line
let t1 = C.convert(t2)
in the alternate decoding phase work, and infer correctly the given types.Now, we can use this generic type like this:
Usage:
We can also use typealisases if a particular combination is used frequently:
That's it. Thanks for reading!
Update (16-10-2019)
I stumbled upon this brilliant suggestion by Jussi Laitinen.
Now, our solution can be cleaner by eliminating the third
Converter
type, and instead requiring our first type to be convertible from the second type. Let's see this in code:Also, converting from
String
toInt
is a lot simpler now, sinceInt
already has a failable initializer that accepts aString
. We just extendInt
to conform to ourConvertible
protocol while stating that the generic/associated typeT
to beString
.Update (29-04-2020)
Fadi suggested a more generic solution to this problem that leaves the converting step to the user.
I like it. Here it is:
The text was updated successfully, but these errors were encountered: