Допустим, вы хотите загрузить и отобразить список пользователей (User) в своем приложении. Каждому User требуется имя и фамилия; отчество не является обязательным.
struct User: Codable {
let firstName: String
let middleName: String?
let lastName: String
}
В идеальном мире вы получите JSON, соответствующий модели.
[
{
"firstName": "John",
"lastName": "Doe"
},
{
"firstName": "Luffy",
"middleName": "D.",
"lastName": "Monkey"
}
]
Мы можем без проблем расшифровать этот JSON. Мы получим массив из двух пользователей.
let json = """...the JSON string above..."""
do {
let users = try JSONDecoder().decode([User].self, from: json.data(using: .utf8)!)
XCTAssertEqual(users.count, 2)
} catch {
print(error)
}
Поведение по умолчанию
Мир не совершенен. Иногда вы можете получить неожиданный формат JSON.
[
{
"firstName": "John",
"lastName": "Doe"
},
{
"firstName": "Luffy",
"middleName": "D.",
"lastName": "Monkey"
},
{
"firstName": "Alice" // <1>
}
]
<1> Отсутствует lastName, которая является обязательным полем.
Когда вы попытаетесь декодировать приведенный выше JSON, вы получите DecodingError.keyNotFound, потому что у последнего объекта нет lastName.
DecodingError
▿ keyNotFound : 2 elements
- .0 : CodingKeys(stringValue: "lastName", intValue: nil)
▿ .1 : Context
▿ codingPath : 1 element
▿ 0 : _JSONKey(stringValue: "Index 2", intValue: 2)
- stringValue : "Index 2"
▿ intValue : Optional<Int>
- some : 2
- debugDescription : "No value associated with key CodingKeys(stringValue: \"lastName\", intValue: nil) (\"lastName\")."
- underlyingError : nil
Поведение декодера по умолчанию рассматривает декодирование массива как атомарную операцию. Массив JSON будет успешно декодирован после декодирования каждого элемента в массиве. Это означает, что один искаженный или поврежденный объект в массиве приведет к сбою декодирования всего массива. Как и в нашем предыдущем примере, отсутствие фамилии в последнем объекте приводит к сбою всего массива.
Я думаю, что это правильное поведение; Спецификация API должна работать как источник правды. Codable уже поддерживает необязательное (?) значение, если какие-либо поля могут быть нулевыми, это должно быть задокументировано в спецификации, чтобы мы могли создать правильную структуру для объекта.
Мы бы объявили наш объект User таким образом, если бы lastName допускало значение NULL.
struct User: Codable {
let firstName: String
let middleName: String?
let lastName: String?
}
Если JSON не соответствует спецификации, операция должна завершиться неудачно как можно быстрее, поэтому мы можем поднять вопрос для бэкэнда, чтобы решить проблему.
Проблема
Такое поведение по умолчанию имеет смысл, если у вас есть полный контроль над API. Если вы этого не сделаете, такое поведение может быть немного резким. Может быть время, когда вы ничего не сможете сделать с API, например, сторонним API, устаревшим API, который отказываются менять разработчики бэкэнда, API с устаревшими поврежденными данными. Когда придет время, вы, возможно, не захотите, чтобы один поврежденный элемент мешал вам отображать остальные действительные.
Решение
Чтобы смягчить проблему, мы создадим объект-оболочку OptionalObject<T>, который работает точно так же, как SwiftOptional.
public struct OptionalObject<Base: Decodable>: Decodable {
public let value: Base?
public init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
self.value = try container.decode(Base.self)
} catch {
self.value = nil
}
}
}
Попробуйте еще раз декодировать искаженный JSON, но на этот раз используйте OptionalObject<User>.
do {
let optionalUsers = try JSONDecoder().decode([OptionalObject<User>].self, from: json.data(using: .utf8)!)
} catch {
print(error)
}
Операция завершится успешно, но мы получим массив OptionalObject<User>, а не User. Мы можем исправить это с помощью compactMap(_:).
compactMap(_ 🙂
Возвращает массив, содержащий ненулевые результаты вызова данного преобразования с каждым элементом этой последовательности.
do {
let optionalUsers = try JSONDecoder().decode([OptionalObject<User>].self, from: json.data(using: .utf8)!)
let users = optionalUsers.compactMap { $0.value }
} catch {
print(error)
}
Теперь пользователи будут массивом из двух допустимых пользовательских объектов.
Массив в другом объекте
Если ваш проблемный массив является свойством другого объекта, скажем, у вас есть компания (Company), в которой есть сотрудники ([User]).
struct Company: Codable {
let employees: [User]
}
Декодирование искаженного JSON приведет к той же ошибке.
let json =
"""
{
"employees":
[
{
"firstName": "John",
"lastName": "Doe"
},
{
"firstName": "Luffy",
"middleName": "D.",
"lastName": "Monkey"
},
{
"firstName": "Alice"
}
]
}
"""
do {
let company = try JSONDecoder().decode(Company.self, from: json.data(using: .utf8)!)
} catch {
print(error)
}
DecodingError
▿ keyNotFound : 2 elements
- .0 : CodingKeys(stringValue: "lastName", intValue: nil)
▿ .1 : Context
▿ codingPath : 2 elements
- 0 : CodingKeys(stringValue: "employees", intValue: nil)
▿ 1 : _JSONKey(stringValue: "Index 2", intValue: 2)
- stringValue : "Index 2"
▿ intValue : Optional<Int>
- some : 2
- debugDescription : "No value associated with key CodingKeys(stringValue: \"lastName\", intValue: nil) (\"lastName\")."
- underlyingError : nil
Решение такое же, как мы обсуждали ранее, но на этот раз мы должны поместить логику декодирования в инициализатор.
struct Company: Codable {
let employees: [User]
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let nullableEmployees = try values.decode([OptionalObject<User>].self, forKey: .employees) // <1>
self.employees = nullableEmployees.compactMap { $0.value } // <2>
}
}
<1> Используйте OptionalObject<User> вместо User.
<2> Отфильтруйте nil значение и снова назначьте пользователей employees.
Просто так и теперь у вас есть работающий объект компании.