
Introduction
What most modern applications have in common is that they need to encode or decode data in various forms. Whether it’s Json data downloaded over the network, or some form of serialized representation of a model stored locally, being able to reliably encode and decode disparate data is essential for almost any Swift code.
That’s why Swift’s Codable API is so important as part of the new features in Swift 4.0. Since then, it has evolved into a standard, robust mechanism for encoding and decoding using Apple’s various platforms, including server-side Swift.
What makes Codable so great is that it is tightly integrated with the Swift toolchain, allowing the compiler to automatically synthesize a lot of code needed to encode and decode various values. However, sometimes you need to customize how values are represented when serialized,
So how do you adjust your Codable implementation to do this?
Codable custom parsing Json
1 Modify Key
You can customize how types are encoded and decoded by modifying the keys used as part of the serialized representation. Suppose a core data model we are developing looks like this:
struct Article: Codable {
var url: URL
var title: String
var body: String
}
Code language: JavaScript (javascript)
Our model currently uses a fully auto-synthesized Codable implementation, which means that all of its serialization keys will match the names of its properties. However, the data from which the Article value will be decoded (eg Json downloaded from the server) may use a slightly different naming convention, causing the default decoding to fail. Fortunately, this problem is easy to fix, to customize which keys Codable will use when decoding (or encoding) instances of the Article type, all you have to do is define a CodingKeys enumeration in it, and customize The case of the key matches the assignment of the custom primitive value. As follows:
extension Article {
enum CodingKeys: String, CodingKey {
case url = "source_link"
case title = "content_name"
case body
}
}
Code language: JavaScript (javascript)
By doing the above, you can continue to do the actual coding work with the default implementation generated by the compiler, while still being able to change the names of the keys that will be used for serialization. While the above technique is great for when we want to use a completely custom key name, if we just want the Codable to use the snake_case version of the property name (eg, convert backgroundColor to background_color), then we can simply change the Json decoder’s keyDecodingStrategy :
var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
Code language: JavaScript (javascript)
The beauty of the above two APIs is that they are able to resolve mismatches between Swift models and the data used to represent them without modifying property names.
2 Ignore Key
It’s really useful to be able to customize the names of encoded keys, but sometimes we might want to ignore certain keys entirely.
For example, a note-taking application is currently being developed and enables users to group various notes together to form a NoteCollection
that can include localdrafts
:
struct NoteCollection: Codable {
var name: String
var notes: [Note]
var localDrafts = [Note]()
}
Code language: JavaScript (javascript)
However, while it’s certainly convenient to include localDrafts
in a NoteCollection
model, we don’t want to include these drafts when serializing or deserializing such a collection.
The reason for this could be to give the user a neat state every time the app is launched, or because the server doesn’t support drafts.
Fortunately, this can also be done easily without having to change the actual Codable implementation of the NoteCollection
, if you define a CodingKeys
enum as before, but just omit localDrafts
, then the NoteCollection
value will not be taken into account when encoding or decoding the NoteCollection
value. Attributes:
extension NoteCollection {
enum CodingKeys: CodingKey {
case name
case notes
}
}
Code language: JavaScript (javascript)
For the above to function properly, the properties to be omitted must have default values, localDrafts
already has default values.
3 Create a matching structure
So far it’s just been tweaking the type’s encoded keys, and while this can often be of great benefit, sometimes further tweaking of the Codable customization is required.
Suppose you’re building an app that includes currency conversion functionality, and you’re downloading the current exchange rate for a given currency as Json data, like this:
{
"currency": "PLN",
"rates": {
"USD": 3.76,
"EUR": 4.24,
"SEK": 0.41
}
}
Code language: JSON / JSON with Comments (json)
Then, in Swift code, I want to convert such Json response into CurrencyConversion
instances, each containing an array of ExchangeRate
entries, one for each currency:
struct CurrencyConversion {
var currency: Currency
var exchangeRates: [ExchangeRate]
}
struct ExchangeRate {
let currency: Currency
let rate: Double
}
Code language: JavaScript (javascript)
However, simply making both of the above models conform to Codable will again cause the Swift code to not match the Json data to be decoded. But this time, it’s not just a matter of keyword names, the structure is fundamentally different. Of course, it is possible to modify the structure of the Swift model to exactly match the structure of the Json data, but this is not always possible. While it is important to have the correct serialization code, it is equally important to have a model structure that fits the actual codebase.
Instead, create a new specialized type that will bridge the gap between the format used in Json data and the structure of Swift code. In this type, we will be able to encapsulate all the logic needed to convert a Json exchange rate dictionary into a series of ExchangeRate
models, like this:
private extension ExchangeRate {
struct List: Decodable {
let values: [ExchangeRate]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String : Double].self)
values = dictionary.map { key, value in
ExchangeRate(currency: Currency(key), rate: value)
}
}
}
}
Code language: JavaScript (javascript)
Using the above types, it is now possible to define a private property whose name matches the Json key used for its data, and make the exchangeRates
property just act as a public-facing proxy for the private property:
struct CurrencyConversion: Decodable {
var currency: Currency
var exchangeRates: [ExchangeRate] {
return rates.values
}
private var rates: ExchangeRate.List
}
Code language: PHP (php)
The reason the above works is that computed properties are never considered when encoding or decoding the value. The above technique can be a great tool when wanting to make our Swift code compatible with a Json API that uses a very different structure, without having to implement Codable completely from scratch.
4 Conversion value
A very common problem when decoding, especially with external Json APIs that you have no control over, is encoding types in a way that is incompatible with Swift’s strict type system. For example, the Json data to be decoded might use strings to represent integers or other types of numbers.
Let’s look at a way we can handle these values, again in a self-contained way that doesn’t require us to write a completely custom Codable implementation. Essentially what I want to do is to convert a string value to another type, in the case of Int
, I would start by defining a protocol that can mark any type as StringRepresentable
, which means it can be converted to a character String representation, or convert it from string representation to the desired type:
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
Code language: JavaScript (javascript)
Next, create another specialized type, which is for any value that can be backed by a string, and let it contain all the code needed to decode and encode a value to and from a string:
struct StringBacked<Value: StringRepresentable>: Codable {
var value: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Failed to convert an instance of \(Value.self) from '\(string)'"
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}
Code language: JavaScript (javascript)
Just like how you used to create private properties for Json-compatible underlying storage, you can now do the same for any property that is encoded by a string backend, while still exposing the data appropriately to other Swift code types, which is a great idea for An example of how the numberOfLikes property of a video type does this:
struct Video: Codable {
var title: String
var description: String
var url: URL
var thumbnailImageURL: URL
var numberOfLikes: Int {
get { return likes.value }
set { likes.value = newValue }
}
private var likes: StringBacked<Int>
}
Code language: JavaScript (javascript)
There’s definitely a trade-off here between the complexity of having to manually define setters and getters for properties and having to fall back to a fully custom Codable implementation, but for types like the Video struct above, where it’s only Having a property that needs to be customized, using a private backing property might be a good option.
Codable parses any type into the desired type
1 General analysis
By default, when parsing Json using Swift’s built-in Codable API, the property type needs to be consistent with the type in Json, otherwise parsing will fail.
For example, there is the following Json:
{
"name":"ydw",
"age":18
}
Code language: JavaScript (javascript)
The models commonly used in development are as follows:
struct User: Codable {
var name: String
var age: Int
}
Code language: JavaScript (javascript)
At this time, there is no problem with normal parsing, but:
When the server returns 18 in age in String mode “18”, it cannot be parsed, which is a very rare situation;
Another common one is to return “18.1”, which is a Double type, which cannot be successfully parsed at this time.
When using OC, the common method is to parse it into NSString type, and then convert it when using it, but when using Swift’s Codabel, this cannot be done directly.
2 If the server only returns Age as String, and can confirm whether it is Int or Double
This can be done using the “value conversion” above:
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
struct StringBacked<Value: StringRepresentable>: Codable {
var value: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let string = try container.decode(String.self)
guard let value = Value(string) else {
throw DecodingError.dataCorruptedError(
in: container,
debugDescription: "Failed to convert an instance of \(Value.self) from '\(string)'"
)
}
self.value = value
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(value.description)
}
}
Code language: JavaScript (javascript)
At this time, the model is as follows:
{
"name":"zhy",
"age":"18"
}
struct User: Codable {
var name: String
var ageInt: Int {
get { return age.value }
set { age.value = newValue}
}
private var age: StringBacked<Int>
}
Code language: JavaScript (javascript)
3 Not sure what type it is
The first processing method will change the original data structure. Although it has more versatility for the parsing process of directly rewriting User, it is helpless in other situations. The second method is also not implemented by rewriting the parsing process of the model itself, which is not universal and is too troublesome, and needs to be repeated every time it is encountered.
Referring to the first method, first write a method that converts any type to String?:
@propertyWrapper public struct YDWString: Codable {
public var wrappedValue: String?
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
var string: String?
do {
string = try container.decode(String.self)
} catch {
do {
try string = String(try container.decode(Int.self))
} catch {
do {
try string = String(try container.decode(Double.self))
} catch {
string = nil
}
}
}
wrappedValue = string
}
}
Code language: JavaScript (javascript)
At this point User writes:
struct User: Codable {
var name: String
@YDWString public var age: String?
}
Code language: JavaScript (javascript)
In the same way, you can write a YDWInt
to convert any type to Int
. If it cannot be converted, you can control it to be nil or directly equal to 0, so as to ensure that no matter what, the parsing will not fail. At this point User writes:
struct User: Codable {
var name: String
@YDWInt public var age: Int
}
Code language: JavaScript (javascript)
It seems that this place has little impact, only the User parsing failure is nothing. When the entire page is returned with a Json, no matter which part of the problem occurs, it will cause the actual page parsing to fail, so it is still necessary to do the best compatibility operation. it is good.