preface
Json parsing is one of the most common issues in mobile development. On the other hand, thanks to Swift’s empty security and let immutability, the reliability of code logic is greatly guaranteed. For example, if the field “A” is empty, the local code needs to be processed according to its default value. This scenario is common in some configuration-related business logic. Of course, there are various solutions to this problem, but the most straightforward and maintainable solution is to solve the problem from the source, which is to replace the default properties with the default values during JSON parsing. This is the same technique that this article explores in conjunction with JSON parsing using the Codable protocol.
Json decode comes with Codable
Let’s start with an example of json as follows:
{
"id": 12345,
"title": "abcd",
"isA": true
}
Copy the code
class Node: NSObject.Codable {
let id: Int
let title: String
let isA: Bool
}
Copy the code
Normally, the class extends the Codable protocol, because the custom class attributes are required by default so that the init(from decoder: decoder) constructor is not displayed.
Codable JSON parsing relies on the init(from decoder: decoder) constructor.
If we define “isA” as optional. In order for json parsing to work, there are two methods: 1. ; 2, override init(from decoder: decoder) constructor.
- Method 1:
class Node: NSObject.Codable {
let id: Int
let title: String
let isA: Bool?
}
Copy the code
- Method 2:
class Node: NSObject.Codable {
let id: Int
let title: String
let isA: Bool
required init(from decoder: Decoder) throws{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.isA = try container.decodeIfPresent(Bool.self, forKey: .isA) ?? true}}Copy the code
Obviously, method 2 is friendlier for processing logic that is externally referenced to the Node.isA property, because there is null processing logic. If there is an optional field that needs to be processed, then you have to rewrite the entire init(from decoder: decoder) to parse all the other fields in the other properties. The result is less efficient coding and higher maintenance costs…
Xcode extensions generate data entities
What can be done to improve the coding efficiency of the above scheme (rewrite init(from decoder: decoder)? Since code encapsulation is not easy to solve, the author’s first thought is code generation. Of course, there are many ways to generate code, but I chose the Xcode extension because I don’t think there are many scenarios where you need to write a lot of similar code, and Xcode extensions are lightweight and less coupled to the project itself.
JsonGenerator
JsonGenerator is an Xcode extension project written by the author according to his own needs. It consists of two steps: Rule and Entry.
- Rule
Due to the Swift feature, the attributes of the Model cannot be represented in JSON. We need to convert json to a custom rule:
let/var:Key:Type(:? :Default)
- Rules are separated by colons (:).
- The let/var attribute is declared as let or var.
- Key Property name. The default value is the field name in json.
- Type Indicates the attribute Type. If the attribute Type is user-defined, it is displayed as xxxNode. If the property is a collection, it is displayed as xxxArrayNode.
- ? Property is the default value corresponding to mandatory scenarios for Codable JSON parsing.
- Default Indicates the Default value of the attribute.
- The name of the first behavior of each type is separated by \n
Specific effects:
- Entry
Generate an entity according to Rule. If 😕 The init(from decoder: decoder) constructor is automatically added to handle Default scenarios.
Specific effects:
This project is written by the author impromptu, the code logic design is not perfect, here is just to provide an idea. Can write a similar tool according to the actual needs, in the daily development can also improve a lot of efficiency.
Let’s talk about overriding init(from decoder: decoder) to handle defaults. Codable is an advantage for interpreting JSON in cidcidr, which is viewable for developers. In addition, developers can customize their OWN rules for interpreting JSON in Codable. But the ability to customize is a relative thing, and in most scenarios the format of JSON directly corresponds to the structure of the data entity. For developers who don’t have to worry too much about how JSON is parsed, the advantages mentioned above are likely to become disadvantages. Also, if the data entity contains too many properties, init(from decoder: decoder) contains more property initialization code. On the other hand, this method needs to be maintained whenever a new field is added. The result may be:
- Repetitive code logic increases.
- Once a specification is not established in team development, the maintenance cost increases significantly after the addition of fields, which may affect the management of other business code.
Property Wrapper
Swift has added a new Property Wrapper, called Property Wrapper, in a higher version (Xcode11 is available for review, but not yet). With this feature, we can encapsulate many interesting code tools and simplify coding logic. Property Wrapper is also a bit like annotations in Java, but of course it’s not nearly as powerful as annotations. I was also inspired to use Property Wrapper to set default values for Codable decoding. There is also a detailed description of how to handle default values using Property Wrapper. The author sums up this plan roughly below.
// Define the DefaultValue protocol, specifying the default values for each type
protocol DefaultValue {
associatedtype Value: Codable
static var defaultValue: Value { get}}@propertyWrapper
struct Default<T: DefaultValue> {
var wrappedValue: T.Value
}
// Override Codable decode to call decodeIfPresent when paradigm T integrates DefaultValue
extension KeyedDecodingContainer {
func decode<T> (_ type: Default<T>.Type.forKey key: Key
) throws -> Default<T> where T: DefaultValue {
try decodeIfPresent(type, forKey: key) ?? Default(wrappedValue: T.defaultValue)
}
}
extension Default: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = (try? container.decode(T.Value.self)) ?? T.defaultValue
}
}
// Bool resolves to false by default
extension Bool: DefaultValue {
static let defaultValue = false
}
class Node: NSObject.Codable {
let id: Int
let title: String
// define the isA attribute as Bool. The defaultValue is Bool. DefaultValue = false
@Default<Bool> var isA: Bool
override var description: String {
return """
{
"id": \(id),
"title": \(title),
"isA": \(isA)} "" "}}Copy the code
For properties defined using a custom Property Wrapper, the logic in the Property Wrapper layer is transparent to the developer and its value is the wrappedValue Property inside the Property Wrapper. For example, if @default
var isA: Bool = true, wrappedValue = true.
Ps: why do I need to define the DefaultValue protocol to specify the DefaultValue? This is because the custom Property Wrapper is itself a custom type, but the compiler flattens the hierarchy. @default
var isA: Bool is instantiated on Node. IsA: Bool isA: Bool isA: Bool isA: Bool isA: Bool isA: Bool isA: Bool Therefore, the default value should be defined as static, which makes sense for code design.
The advantage of this scheme is that there is no need to decode a large number of properties compared to overwriting init(from decoder: decoder), only for a single property, and the code logic is decoupled. The disadvantages are:
- Because additional default values need to be defined, the json hierarchy becomes more complex as the number of custom types increases, and static definitions may need to be defined more than once for each type. Maintenance costs are a problem.
- A variable declared by the Property Wrapper typeCan only use
var
The Swift immutable feature cannot be enjoyed(I’m a stickler for immutable features…) . Ps: This situation can also be changedDefault.wrappedValue
uselet
Declaration, so that even if the outside isvar
Keyword declarations also cannot change values. But this approach is less flexible.
JSONDecoder
Is it possible to build a wheel to gracefully handle defaults? For example, modify JSONDecoder source code or custom JSONDecoder? First, the results of my research: No. Then let’s take a look at some of the JSONDecoder source code to see how Codable implements JSON parsing. JSONEncoder. Swift
/ / JSONEncoder. Swift, 1200 lines
open func decode<T : Decodable> (_ type: T.Type.from data: Data) throws -> T {
let topLevel: Any
do {
topLevel = try JSONSerialization.jsonObject(with: data, options: .fragmentsAllowed)
} catch {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: error))
}
let decoder = __JSONDecoder(referencing: topLevel, options: self.options)
guard let value = try decoder.unbox(topLevel, as: type) else {
throw DecodingError.valueNotFound(type, DecodingError.Context(codingPath: [], debugDescription: "The given data did not contain a top-level value."))}return value
}
.
/ / JSONEncoder. Swift, 2489 lines
func unbox_(_ value: Any.as type: Decodable.Type) throws -> Any? {
if type = = Date.self || type = = NSDate.self {
return try self.unbox(value, as: Date.self)}else if type = = Data.self || type = = NSData.self {
return try self.unbox(value, as: Data.self)}else if type = = URL.self || type = = NSURL.self {
guard let urlString = try self.unbox(value, as: String.self) else {
return nil
}
guard let url = URL(string: urlString) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Invalid URL string."))}return url
} else if type = = Decimal.self || type = = NSDecimalNumber.self {
return try self.unbox(value, as: Decimal.self)}else if let stringKeyedDictType = type as? _JSONStringDictionaryDecodableMarker.Type {
return try self.unbox(value, as: stringKeyedDictType)
} else {
self.storage.push(container: value)
defer { self.storage.popContainer() }
return try type.init(from: self)}}.
// developer call
try! JSONDecoder().decode(T.self, from: data)
Copy the code
- JSONDecoder is called when developers need json parsing
decode
Methods. - The interior actually depends on
JSONSerialization
Parse the passed data into a dictionary. - in
unbox_
The end of the methodtry type.init(from: self)
Theta corresponds to thetainit(from decoder: Decoder)
. - The following can be understood as JSONDecoder’s JSON parsing is based on the custom type of association to form a tree, and then depth first traversal, and then step by step out of the corresponding JSON data entities.
To sum up, Codable JSON parsing still relies on the init(from decoder: decoder) constructor. Then the aforementioned idea of processing default values during parsing cannot bypass this mechanism.
Ps: To interrupt a topic, in the study of JSONDecoder, saw a friend through custom JSONDecoder to optimize the performance of the analysis. After JSONSerialization is parsed into a dictionary, the value of JSONSerialization needs to be forced to a specific type, which affects performance. Delve into Decodable – Write a JSON parser that goes beyond native
Ideas for default value definitions
The most desirable solution for developers is to assign the default value directly to the property definition, which can be used automatically when defaulting in JSON. Something like this:
class Node: NSObject.Codable {
let id: Int
let title: String
let isA: Bool = true
}
Copy the code
Unfortunately, the Swift compiler does not allow an assignment in the constructor after the let attribute declaration. Something like this:
class Node: NSObject.Codable {
let id: Int
let title: String
let isA: Bool = true
override init(a) {
self.id = 0
self.title = ""
self.isA = false // Error: Immutable value 'self.isA' may only be initialized once}}Copy the code
By the same token, the init(from decoder: decoder) constructor is automatically generated for us after the Codable protocol is integrated. The isA attribute initialization does not appear in init(from decoder: decoder) if defined above.
class Node: NSObject.Codable {
let id: Int
let title: String
let isA: Bool = true
required init(from decoder: Decoder) throws{
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.title = try container.decode(String.self, forKey: .title)
self.isA = try container.decode(Bool.self, forKey: .isA) // Error: Immutable value 'self.isA' may only be initialized once}}Copy the code
This is why even if you have a value for isA in JSON, only isA = true is always resolved. Are there any other third-party JSON parsing libraries that can meet this requirement? HandyJSON is possible. To circumvent this mechanism, it writes directly to memory.
Because this article is focused on Codable, check out this article for HandyJSON design ideas
About the compatibility of GRDB database migration
Finally, a brief question similar to this one. GRDB database queries are also Codable, so entities are constructed by calling init(from decoder: decoder). After database migration, new fields that do not exist in the old data will not be parsed, so you can also avoid this problem by compatibility with default values in the init(from decoder: decoder) phase.
The last
This article summarizes how to handle defaults when parsing JSON using the Codable protocol. For now, Swift’s Codable design is a little too conservative to handle defaults gracefully. If there is a better plan, also welcome to leave a message we discuss together!