1, Foreword
- What most modern applications have in common is that they need to encode or decode various forms of data. Whether it is the Json data downloaded through the network or some form of serialized representation of the model stored locally, it is essential for almost any Swift code base to reliably encode and decode different data.
- This is why Swift's Codable API is so important when it becomes part of the new features of Swift 4.0. Since then, it has developed into a standard and robust mechanism for encoding and decoding in Apple's various platforms, including server-side swift.
- Codable is so good because it is tightly integrated with the Swift tool chain, so that the compiler can automatically synthesize a large amount of code required to encode and decode various values. However, sometimes you need to customize the representation of serialization time values, so how can you adjust the codable implementation to do this?
2, Codable custom parsing Json
① Modify Key
- You can customize the encoding and decoding of types by modifying the keys used as part of the sequenced representation. Suppose we are developing a core data model as follows:
struct Article: Codable {
var url: URL
var title: String
var body: String
}
- Our model currently uses a fully auto composed Codable implementation, which means that all its serialization keys will match the name of its attribute. However, the data from which the Article value will be decoded (for example, Json downloaded from the server) may use a slightly different naming convention, resulting in a default decoding failure. Fortunately, this problem is easy to solve. To customize which keys Codable will use when decoding (or encoding) an instance of Article type, all you have to do is define a CodingKeys enumeration in it and assign a custom original value for the case matching the key you want to customize. As follows:
extension Article {
enum CodingKeys: String, CodingKey {
case url = "source_link"
case title = "content_name"
case body
}
}
- Through the above operations, you can continue to use the default implementation generated by the compiler for actual coding, while still changing the name of the key to be used for serialization. Although the above technology is very suitable when we want to use fully customized key names, if we only want Codable to use snake with attribute names_ Case version (for example, converting background color to background_color), you can simply change the keyDecodingStrategy of the Json decoder:
var decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
- The advantage of the above two API s is that they can solve the mismatch between the Swift model and the data used to represent them without modifying the attribute name.
② Ignore Key
- It's useful to be able to customize the names of encoding keys, but sometimes we may want to ignore some keys completely. For example, a note taking application is being developed and enables users to group various notes together to form a NoteCollection that can include local drafts:
struct NoteCollection: Codable {
var name: String
var notes: [Note]
var localDrafts = [Note]()
}
- However, while it is convenient to include localDrafts in the NoteCollection model, it can be said that we do not want to include these drafts when serializing or deserializing such collections. The reason for this may be to provide users with a clean state every time they start the application, or because the server does not support drafts. Fortunately, this can also be done easily without changing the actual codeable implementation of NoteCollection. If a CodingKeys enumeration is defined as before and localDrafts is omitted, this property will not be considered when encoding or decoding the NoteCollection value:
extension NoteCollection {
enum CodingKeys: CodingKey {
case name
case notes
}
}
- In order for the above functions to work properly, the attribute to be omitted must have a default value. In this case, localDrafts already has a default value.
③ Create matching structure
- So far, we have only adjusted the encoding keys of types. Although this can usually benefit a lot, it sometimes needs to make further adjustments to Codable customization. Suppose an application with currency conversion function is being built, and the current exchange rate of a given currency is being downloaded as Json data, as shown below:
{
"currency": "PLN",
"rates": {
"USD": 3.76,
"EUR": 4.24,
"SEK": 0.41
}
}
- Then, in Swift code, you want to convert this kind of Json response into CurrencyConversion instances. Each instance contains an array of ExchangeRate entries, and each currency corresponds to one:
struct CurrencyConversion {
var currency: Currency
var exchangeRates: [ExchangeRate]
}
struct ExchangeRate {
let currency: Currency
let rate: Double
}
- However, if only the above two models comply with Codable, the Swift code will again not match the Json data to be decoded. But this time, it's not just the keyword name, but the structure is fundamentally different. Of course, you can modify the structure of the Swift model to exactly match the structure of the Json data, but this is not always feasible. Although it is important to have the correct serialization code, it is equally important to have a model structure suitable for the actual code base.
- Instead, create a new private type that bridges the format used in Json data to the structure of Swift code. In this type, we will be able to encapsulate all the logic required to convert the Json exchange rate dictionary into a series of exchange rate models, as follows:
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)
}
}
}
}
- Using the above types, you can now define a private property whose name matches the Json key used for its data, and make the exchangeRates property act only 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
}
- The reason why the above method works is that computational properties are never considered when encoding or decoding values. When we want to make our Swift code compatible with the Json API using a very different structure, the above technology may be a good tool without completely implementing Codable from scratch.
④ Conversion value
- When decoding, especially when using uncontrollable external Json API s, a very common problem is to encode types in a manner incompatible with Swift's strict type system. For example, the Json data to be decoded may use strings to represent integers or other types of numbers.
- Let's look at a way to handle these values. Again, in a self-contained way, it doesn't require us to write a fully custom Codable implementation. Essentially, what you want to do is to convert the string value to another type. Taking Int as an example, you will start by defining a protocol that can mark any type as StringRepresentable, which means that it can be converted to string representation or from string representation to the required type:
protocol StringRepresentable: CustomStringConvertible {
init?(_ string: String)
}
extension Int: StringRepresentable {}
- Next, create another private type, which is for any value that can be supported by a string, and let it contain all the code required 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)
}
}
- Just like the previous method of creating private attributes for Json compatible basic storage, you can now perform the same operation on any attribute encoded by the back end of the string, while still appropriately exposing the data to other Swift code types. This is an example of performing this operation on the numberOfLikes attribute of a video type:
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>
}
- There must be a compromise between the complexity of having to manually define setter s and getter s for attributes and the complexity of having to fall back to the fully customized Codable implementation. However, for the type of Video structure mentioned above, it only has one attribute that needs to be customized, and using private support attributes may be a good choice.
3, Codable resolves any type to the desired type
① General analysis
- By default, when parsing Json using Swift's built-in Codable API, the attribute type must be consistent with the type in Json, otherwise the parsing will fail.
- For example, the existing Json is as follows:
{
"name":"ydw",
"age":18
}
- The models commonly used in development are as follows:
struct User: Codable {
var name: String
var age: Int
}
- At this time, there is no problem with normal parsing, but:
-
- When the server returns 18 in the age in the String mode of "18", it cannot be parsed, which is very difficult to meet;
-
- Another common method is to return "18.1", which is a Double type. At this time, it cannot be successfully resolved.
- When using OC, the commonly used method resolves it to NSString type, and then converts it. However, when using Swift's codebel, this cannot be done directly.
② If the server only returns Age in String mode, it can confirm whether it is Int or Double
- You can do this 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)
}
}
- 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>
}
③ Unable to confirm what type it is
- The first processing method will change the original data structure. Although it has more generality for the parsing process of directly rewriting the User, it is helpless in other cases. The second method will not be implemented by rewriting the analysis process of the model itself, which is not universal and too troublesome. It needs to be done every time.
- Referring to the first method, first write a method to convert any type into String? How to:
// If you are not sure what type the server returns, convert it to String and ensure normal parsing
// Double Int String is currently supported
// Other types resolve to nil
//
///Resolve String Int Double to String? Wrapper for
@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 {
// If you don't want string? String can be assigned a value of = "" here
string = nil
}
}
}
wrappedValue = string
}
}
- At this time, the User is written as:
struct User: Codable {
var name: String
@YDWString public var age: String?
}
- Similarly, you can write a ydbint to convert any type to Int. if it cannot be converted, you can control it to nil or directly equal to 0, so as to ensure that the resolution will not fail anyway. At this time, the User is written as:
struct User: Codable {
var name: String
@YDWInt public var age: Int
}
- It seems that this place has little impact. Only the User parsing failure is nothing. When the whole page is returned with a Json, no matter which part has a problem, it will lead to the real page parsing failure. Therefore, it is best to do a good job of compatibility operation.