When developing mobile applications, one way or another, we are faced with the need to parse server data into internal application models. In the vast majority of cases, this data comes in JSON format. Starting with Swift 4, the main tool for parsing JSON is to use a protocol Decodable
and an object JSONDecoder
.
This approach greatly simplified the process of parsing data and reduced the number of boilerplate code. In most cases, itβs quite simple to create models with properties named just like fields in the JSON object and JSONDecoder
will do the rest for you. Minimum code, maximum benefit. However, this approach has one drawback, namely, the extremely low parser loyalty. I will explain. If there is any discrepancy between the internal data model (Decodable objects) and what came in JSON,JSONDecoder
throws an error and we lose the whole object. Perhaps, in some situations, this model of behavior is preferable, especially when it comes, for example, to financial transactions. But in many cases it would be useful to make the parsing process more loyal. In this article I would like to share my experience and talk about the main ways to increase this same loyalty.
Filtering invalid objects
Well, the first item is, of course, filtering invalid objects. In many situations, we do not want to lose the entire object if one of the nested objects is not valid. This applies to both single objects and arrays of objects. I will give an example. Suppose we make an application for selling goods and on one of the screens we get a list of goods in approximately this form.
{
"products": [
{...},
{...},
....
]
}
, . . , , - . , JSONDecoder
β β .
, :
{
"id": 1,
"title": "Awesome product",
"price": 12.2,
"image": {
"id": 1,
"url": "http://image.png",
"thumbnail_url": "http://thumbnail.png"
}
}
, , image
. , , image , image = nil
. , , JSONDecoder
.
, JSONDecoder
2 : decode
decodeIfPresent
. optional
, nil
, , null
. .
, , . , init(decoder)
try?
nil
. , boilerplate . .
struct FailableDecodable<Value: Decodable>: Decodable {
var wrappedValue: Value?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try? container.decode(Value.self)
}
}
struct FailableDecodableArray<Value: Decodable>: Decodable {
var wrappedValue: [Value]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements: [Value] = []
while !container.isAtEnd {
if let element = try? container.decode(Value.self) {
elements.append(element)
}
}
wrappedValue = elements
}
}
.
struct ProductList: Decodable {
var products:FailableDecodableArray<Product>
}
struct Product: Decodable {
let id: Int
let title: String
let price: Double
let image: FailableDecodable<Image>?
}
struct Image: Decodable {
let id: Int
let url: String
let thumbnailUrl: String
}
. , .
let products = productsList.products.wrappedValue
let image = products.first?.image.wrappedValue
FailableDecodableArray
. , RandomAccessCollection
MutableCollection
, wrappedValue
. FailableDecodable
. , , computed property
, , . , .
@propertyWrapper
Swift 5.1 β @propertyWrapper
.
@propertyWrapper
struct FailableDecodable<Value: Decodable>: Decodable {
...
}
@propertyWrapper
struct FailableDecodableArray<Value: Decodable>: Decodable {
...
}
struct ProductList: Decodable {
@FailableDecodableArray
var products:[Product]
}
struct Product: Decodable {
let id: Int
let title: String
let price: Double
@FailableDecodable
let image:Image?
}
, wrappedValue
. , , , , :)
, , optional
.
@FailableDecodable
let image:Image?
let image: FailableDecodable<Image>
, optional
Image?
, wrappedValue
optional
, .
Swift
@FailableDecodable?
let image:Image?
, , JSON nil
. @propertyWrapper
, 100% JSON.
@dynamicMemberLookup
dynamicMemberLookup
.
@dynamicMemberLookup
struct FailableDecodable<Value: Decodable>: Decodable {
var wrappedValue: Value?
subscript<Prop>(dynamicMember kp: KeyPath<Value, Prop>) -> Prop {
wrappedValue[keyPath: kp]
}
subscript<Prop>(dynamicMember kp: WritableKeyPath<Value, Prop>) -> Prop {
get {
wrappedValue[keyPath: kp]
}
set {
wrappedValue[keyPath: kp] = newValue
}
}
}
2 subscript
, readonly , read/write . .
struct ProductList: Decodable {
var products:FailableDecodableArray<Product>
}
struct Product: Decodable {
let id: Int
let title: String
let price: Double
let image: FailableDecodable<Image>?
}
@propertyWrapper
wrappedValue
, .
let imageURL = products.first?.image.url
, optional
. , , wrappedValue
, , .
products.first?.image.load()
products.first?.image.wrappedValue.load()
( ), .
β . - , ( JSON) Swift , β1β 1 β . JSONDecoder
, , . . , Product
.
{
"id": 1,
"title": "Awesome product",
"price": "12.2",
"image": {
"id": 1,
"url": "http://image.png",
"thumbnail_url": "http://thumbnail.png"
}
}
, , , β , , , , , . , .
struct Convertible<Value: Decodable & LosslessStringConvertible>: Decodable {
var wrappedValue: Value
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let stringValue = try? container.decode(String.self),
let value = Value(stringValue) else {
wrappedValue = try container.decode(Value.self)
return
}
wrappedValue = value
}
}
struct Product: Decodable {
let id: Int
let title: String
let price: Convertible<Double>
let image: FailableDecodable<Image>?
}
, . FailableDecdable
@propertyWrapper
@dynamicMemberLookup
.
( ) . , , API - , , - . , , , , -, , , -, , . , , .
struct StringConvertible: Decodable {
var wrappedValue: String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
guard let number = try? container.decode(Double.self) else {
wrappedValue = try container.decode(String.self)
return
}
wrappedValue = "\(number)"
}
}
Summarizing all of the above, I would like to note that the emergence of the protocol Decodable
in conjunction with the class JSONDecoder
significantly simplified our life in what is associated with the parsing of server data. However, it is worth noting that JSONDecoder
at the moment it has extremely low loyalty and to improve it (if necessary) you need to work a little and write a few wrappers. I think that in the future all these features will be realized in the object itself JSONDecoder
, because relatively recently he did not even know how to convert keys from snakecase to camelcase and string to date, and now all this is available βout of the boxβ.