So I got a pretty hard problem to tackle. My JSON code has a pretty weird structure. It has the following structure:
{
"title": [
[
"Temperature",
"9 \u00b0C (283 \u00b0F)",
"Good"
],
[
"Visibility",
"10 KM (6.2 Mi)",
"Good"
]
]
}
With the following code I was able to print out some easy json code:
import UIKit
struct WeatherItem: Decodable {
let title: String?
let value: String?
let condition: String?
}
class ViewController: UIViewController {
override func viewDidLoad() {
super.viewDidLoad()
let jsonUrlString = "http://somelinkhere"
guard let url = URL(string: jsonUrlString) else { return }
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else { return }
do {
let weather = try JSONDecoder().decode(WeatherItem.self, from: data)
print(weather.title)
} catch let jsonErr{
print("error", jsonErr)
}
}.resume()
}
}
But the problem is that my output for all 3 variables, title, value and condition nil is.
I am sure I have to change the struct code, but I don't know in what way.
How do I get to JSON code with no title?
You will have to write the decoding initializer by yourself:
struct WeatherData: Decodable {
let title: [WeatherItem]
}
struct WeatherItem: Decodable {
let title: String?
let value: String?
let condition: String?
public init(from decoder: Decoder) throws {
// decode the value for WeatherItem as [String]
let container = try decoder.singleValueContainer()
let components = try container.decode([String].self)
title = components.count > 0 ? components[0] : nil
value = components.count > 1 ? components[1] : nil
condition = components.count > 2 ? components[2] : nil
}
}
let json = """
{
"title": [
["Temperature", "9", "Good"],
["Visibility", "10 KM (6.2 Mi)", "Good"]
]
}
"""
let jsonData: Data = json.data(using: .utf8)!
let decoder = JSONDecoder()
let decoded = try! decoder.decode(WeatherData.self, from: jsonData)
debugPrint(decoded)
Correct json
{
"title": [
[
"Temperature",
" ",
"Good"
],
[
"Visibility",
"10 KM (6.2 Mi)",
"Good"
]
]
}
var arr = [WeatherItem]()
do {
let res = try JSONDecoder().decode([String:[[String]]].self, from: data)
let content = res["title"]!
content.forEach {
if $0.count >= 3 {
arr.append(WeatherItem(title:$0[0],value:$0[1],condition:$0[2]))
}
}
print(arr)
} catch {
print(error)
}
Discussion : your root object is a dictionary that contains 1 key named title and it's value is an array of array of strings or from the model logic it's an array of model named WeatherItem but isn't structured properly for it , so using this
let weather = try JSONDecoder().decode(WeatherItem.self, from: data)
won't work as the current json does't contain keys value and condition
A proper strcuture would be
[
{
"title":"Temperature" ,
"value":"",
"condition":"Good"
},
{
"title":"Visibility",
"title":"10 KM (6.2 Mi)",
"condition":"Good"
}
]
and that will enable you to do
let weather = try JSONDecoder().decode([WeatherItem].self, from: data)
Related
My project includes dynamic data, that changes when controllers change etc, so at one point my data may be:
[
{
"callDescription":"TEST 16/12",
"callDuration":"5-8 Minutes",
"callID":0,
"callMade":false,
"callMade_dateTime":"false_1608151560.0",
"dateTime":1608044666,
"type":"Breakfast Call"
},
{
"callDescription":"TEST 16/12",
"callDuration":"5-8 Minutes",
"callID":0,
"callMade":false,
"callMade_dateTime":"false_1608151560.0",
"dateTime":1608044666,
"type":"Breakfast Call"
},
]
Then a piece of code is ran and my data is now
[
{
"callDescription":"TEST 16/12",
"callDuration":"5-8 Minutes",
"callID":0,
"callMade":false,
"callMade_dateTime":"false_1608151560.0",
"dateTime":1608044666,
"type":"Breakfast Call"
},
null
]
Which is causing the valueNotFound error when the data is requested again.
What is the best way to skip/handle any indexes that are null?
Here is my API code:
class Service {
static let shared = Service()
let BASE_URL = "https://url.com"
func fetchClient(completion: #escaping ([Calls]) -> ()) {
guard let url = URL(string: BASE_URL) else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
print("Failed to fetch data with error: ", error)
return
}
guard let data = data else {return}
do {
let myDecoder = JSONDecoder()
let calls = try myDecoder.decode([Calls].self, from: data)
completion(calls)
} catch let error {
print("Failed to create JSON with error: ", error)
}
}.resume()
}
Calls model:
struct Calls: Decodable {
let callDescription, callDuration, callMade_dateTime: String
let callID: Int
let dateTime: Date
let callMade: Bool
let type: String
}
Quick solution:
let calls = try myDecoder.decode([Calls].self, from: data)
completion(calls)
=>
let calls = try myDecoder.decode([Calls?].self, from: data)
completion(calls.compactMap{ $0 })
Let's simplify the example (I started writing the answer before you wrote a real JSON working) :
struct Custom: Codable {
let data: String
}
let jsonString = """
[{"data": "Hello"}, {"data": "world"}]
"""
let jsonString2 = """
[{"data": "Hello"}, null, {"data": "world"}]
"""
So, some values might be null inside your JSON. That's where we can use Optional.
func test(json: String) {
do {
print("Testing with [Custom].self: \(json)")
let customs = try JSONDecoder().decode([Custom].self, from: json.data(using: .utf8)!)
print("Result: \(customs)")
} catch {
print("Error: \(error)")
}
}
func test2(json: String) {
do {
print("Testing with [Custom?].self: \(json)")
let customs = try JSONDecoder().decode([Custom?].self, from: json.data(using: .utf8)!)
print("Result with optionals: \(customs)")
let unwrapped = customs.compactMap { $0 }
print("Result unwrapped: \(unwrapped)")
} catch {
print("Error: \(error)")
}
}
test(json: jsonString)
test(json: jsonString2)
test2(json: jsonString)
test2(json: jsonString2)
Output:
$>Testing with [Custom].self: [{"data": "Hello"}, {"data": "world"}]
$>Result: [Custom(data: "Hello"), .Custom(data: "world")]
$>Testing with [Custom].self: [{"data": "Hello"}, null, {"data": "world"}]
$>Error: valueNotFound(Swift.KeyedDecodingContainer<.Custom.CodingKeys>, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 1", intValue: 1)], debugDescription: "Cannot get keyed decoding container -- found null value instead.", underlyingError: nil))
$>Testing with [Custom?].self: [{"data": "Hello"}, {"data": "world"}]
$>Result with optionals: [Optional(.Custom(data: "Hello")), Optional(.Custom(data: "world"))]
$>Result unwrapped: [.Custom(data: "Hello"), .Custom(data: "world")]
$>Testing with [Custom?].self: [{"data": "Hello"}, null, {"data": "world"}]
$>Result with optionals: [Optional(.Custom(data: "Hello")), nil, Optional(.Custom(data: "world"))]
$>Result unwrapped: [.Custom(data: "Hello"), .Custom(data: "world")]
I have the following json data and trying to parse it, but I get number of objects, but object itself in the array all are nil.
I do not want to decode the origin in the following json object.
By the way, the following string is first converted to data and then passed it to parse function below.
Data is follows:
[
[
{"id": "152478", "age": 20},
{"character": "king","isDead":"no", "canMove" :"yes", "origin" :"south africa"}
],
[
{"id": "887541", "age": 22},
{"character": "lion", "isDead":"no", "canMove" :"yes", "origin" :"south america"}
]
]
Models
struct A: Codable {
let id: String?
let age: Int?
enum CodingKeys: String, CodingKey {
case id
case age
}
}
struct B: Codable {
let character, isDead, canMove: String?
enum CodingKeys: String, CodingKey {
case character
case isDead
case canMove
}
}
struct AB :Codable {
let a: A
let b: B
init(from decoder: Decoder) throws {
guard var container = try? decoder.unkeyedContainer() else {
//no error here!!!
fatalError()
}
print(container)
guard let a = try? container.decode(A.self),
let b = try? container.decode(B.self)
else {
// throw since we didn't find A first, followed by B
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [], debugDescription: "The given data was not valid JSON.", underlyingError: nil)
)
}
self.a = a
self.b = b
}
}
ViewModel
private func parse(jsonData : Data){
do {
let decoder = JSONDecoder()
let response = try decoder.decode([AB].self, from: jsonData)
print(response)
}
catch (let error as NSError) {
print(error)
}
}
UPDATE:
By the way, the following code works. I wonder why above code does not handle?
private func parseData(jsonData : Data)
{
do {
response = try JSONSerialization.jsonObject(with: jsonData) as! [[[String: Any]]]
for i in 0..<response.count
{
for j in 0..<response[i].count
{
if j == 0
{
let jsonDat = (try? JSONSerialization.data(withJSONObject:response[i][j]))!
let b = try JSONDecoder().decode(A.self, from: jsonDat)
}
else if j == 1
{
let jsonDatt = (try? JSONSerialization.data(withJSONObject:response[i][j]))!
let a = try JSONDecoder().decode(B.self, from: jsonDatt)
}
}
}
print(response)
}
catch let error as NSError {
print(error)
}
}
UPDATE II:
If I make the following changes [AB] --> [[AB]], and then I call it as follows, it decodes data, but in the array I end up, A object has values, but B nil, or vice versa.
let response = try decoder.decode([[AB]].self, from: jsonData)
guard var container = try? decoder.singleValueContainer() else
{
fatalError()
}
I just tried this stripped-down version of your code in a playground
let json = """
[
[
{"id": "152478", "age": 20},
{"character": "king","isDead":"no", "canMove" :"yes", "origin" :"south africa"}
],
[
{"id": "887541", "age": 22},
{"character": "lion", "isDead":"no", "canMove" :"yes", "origin" :"south america"}
]
]
""".data(using: .utf8)!
struct A: Codable {
let id: String
let age: Int
}
struct B: Codable {
let character, isDead, canMove: String
}
struct AB: Codable {
let a: A
let b: B
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
self.a = try container.decode(A.self)
self.b = try container.decode(B.self)
}
}
do {
let ab = try JSONDecoder().decode([AB].self, from: json)
print(ab.count)
print(ab[0].a.id)
print(ab[0].b.character)
print(ab[1].a.id)
print(ab[1].b.character)
} catch {
print(error)
}
and it works just fine. Maybe this helps figuring out what's going on.
I have a json file and reading it and decode it as follows.
my json file looks like as follows, A and B represents the struct object. I wonder are there a better and effective way to decoding this type of json?
[
[
{"id": "152478", "age": "20"},{"character": "king", "isDead":"no", "canMove" :"yes"}
],
[
{"id": "887541", "age": "22"},{"character": "lion", "isDead":"no", "canMove" :"yes"}
]
]
decoding is as follows:
let url = Bundle.main.url(forResource: "mypew", withExtension: "json")!
do {
let jsonData = try Data(contentsOf: url)
// B -> json[0][0], [1][0]
// A -> json[0][1], [0][1]
response = try JSONSerialization.jsonObject(with: jsonData) as! [[[String: Any]]]
for i in 0..<response.count
{
for j in 0..<response[i].count
{
if j == 0
{
let jsonDat = (try? JSONSerialization.data(withJSONObject:response[i][j]))!
let bElement = try JSONDecoder().decode(B.self, from: jsonDat)
self.bArray.append(bElement)
}
else if j == 1
{
let jsonDatt = (try? JSONSerialization.data(withJSONObject:response[i][j]))!
let aElement = try JSONDecoder().decode(A.self, from: jsonDatt)
self.aArray.append(aElement)
}
}
}
First, make the two objects (let's call them A and B) conform to Decodable:
struct A: Decodable {
var id, age: String
}
struct B: Decodable {
var character, isDead, canMove: String
}
Then, if your structure for the pair of A and B is always the same, i.e. it's always [A, B], then you can decode the pair into its own object. Let's call that object ABPair:
struct ABPair: Decodable {
let a: A
let b: B
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
guard let a = try? container.decode(A.self),
let b = try? container.decode(B.self)
else {
// throw since we didn't find A first, followed by B
throw DecodingError.dataCorruptedError(in: container, debugDescription: "error")
}
self.a = a
self.b = b
}
}
And you can decode the array of pairs as follows:
let decoder = JSONDecoder()
let pairs = try? decoder.decode([ABPair].self, from: jsonData)
print(pairs[1].b.character) // lion
Firstly, you need to create a Codable struct for the model. Then decode the model from the data and not the JSON object. Here the code,
Model:
struct Model: Codable {
let id, age, character, isDead, canMove: String?
}
Decoding:
let url = Bundle.main.url(forResource: "mypew", withExtension: "json")!
do {
let jsonData = try Data(contentsOf: url)
let models = try JSONDecoder().decode([[Model]].self, from: jsonData)
for model in models {
print(model[0].age, model[1].canMove)
}
} catch {
print(error)
}
Goal:
When I select a message from the list of chat messages in the MessageListController with didSelectRowAt, I want the opened chat session in the next ChatDetailController to match the same conversation that was selected. Obviously.
I'm working with WatchKit but it's the same thing in this iOS image. The message with Sophia is selected and the chat with Sophia opens.
I want to pass the json "message_id" i.e. the chatMessageId property from my model. I'm already passing the chatMessageId from the MessageModelto the ChatDetailController with this line presentController(withName: "ChatDetailController", context: messageContext)
Here is a print out of the passed data using messageContext.
Passed context: Optional(HTWatch_Extension.MessageModel(partner: "9859", nickname: "Marco", message: "Have you seen is dog?", city: "Madrid", countryBadgeImageURL: https://i.imgur.com/PJcyle7.jpg, messageListImageURL: https://i.imgur.com/PJcyle7.jpg, chatMessageId: "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ", status: "offline"))
What's the next step? How do I tell the ChatDetailController to populate its table with the conversation that matches the row that was selected?
MessageListController
// ...code...
let messageObject = [MessageModel]()
//var chatObject = [ChatModel]()
// ...code...
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
var messageContext = messageObject[rowIndex]
var chatContext = chatObject[rowIndex]
do {
guard let fileUrl = Bundle.main.url(forResource: "Chats", withExtension: "json") else {
print("File could not be located")
return
}
let data = try Data(contentsOf: fileUrl)
let decoder = JSONDecoder()
let msg = try decoder.decode([ChatModel].self, from: data)
self.chatObject = msg
} catch let error {
print(error)
}
// I got part of this line from a previous question, but these property types do not match.
// No matter what chatObject I create I cannot access it/assign it.
// Also I do not want to mutate messageContext which Xcode is telling me to do.
messageContext.chatMessageId = (chatObject as AnyObject).filter { (dictionaryTemp:[String:String]) -> Bool in
return dictionaryTemp["message_id"] == chatContext.chatMessageId
}
// WatchKit's model presentation method.
presentController(withName: "ChatDetailController", context: messageContext)
}
ChatDetailController
var chats: [ChatModel] = []
var messageModel: MessageModel? {
didSet {
guard let model = messageModel else { return }
partnerLabel.setText(model.nickname)
}
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let message = context as? MessageModel {
self.messageModel = message
print("Passed context: \(String(describing: messageModel))")
} else {
print("Passed context error: \(String(describing: context))")
}
do {
guard let fileUrl = Bundle.main.url(forResource: "Chats", withExtension: "json") else {
print("File could not be located")
return
}
let data = try Data(contentsOf: fileUrl)
let decoder = JSONDecoder()
let msg = try decoder.decode([ChatModel].self, from: data)
self.chats = msg
} catch let error {
print(error)
}
DispatchQueue.main.async(execute: {
self.setupTable(chatMessageArray: self.chats as [AnyObject])
})
}
ChatModel
public struct ChatModel: Codable {
// ... other properties
public var chatMessageId: String
enum CodingKeys: String, CodingKey {
// ... other cases
case chatMessageId = "message_id"
}
init (message:String , fromId:String, toID : String, imgUrl : URL?, chatMessageId : String) {
// ... other properties
self.chatMessageId = chatMessageId
}
}
// .... decoders etc
}
MessageModel
public struct MessageModel: Codable {
// ...
public var chatMessageId: String
enum CodingKeys: String, CodingKey {
// ... other cases
case chatMessageId = "message_id"
}
// ... init/decoders etc
}
Messages.json
[
{
"userid": "4444",
"nickname": "Marco",
"online_status": "offline",
"message": "Have you seen his dog?",
"city": "Madrid",
"flag_url": "https://i.imgur.com/PJcyle7.jpg",
"creationDate": "2016-02-22 15:18:40",
"avatar_url": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ" // different id
},
{
"userid": "12121",
"nickname": "Tom",
"online_status": "online",
"message": "Where is the pizza shop?",
"city": "Kyoto",
"flag_url": "https://i.imgur.com/PJcyle7.jpg",
"creationDate": "2016-02-22 15:18:40",
"avatar_url": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "EnotMkk8REEd0DHGvUgnd45wBap80E" // different id
}
]
Chats.json
2 conversations containing 2 messages each. Each conversation has a unique fromId (partner), toID (myself), and unique message_id.
[
{
"fromId": "zz1234skjksmsjdfwe2zz",
"toId": "qq43922sdkfjsfmmxdfqq",
"messageText": "Have you seen is dog?",
"imageUrl": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ", // conversation 1 - same message id as the 1st message in Messages.josn with nickname Marco.
"read": "true"
},
{
"fromId": "zz1234skjksmsjdfwe2zz",
"toId": "qq43922sdkfjsfmmxdfqq",
"messageText": "Yes I have. It's cute.",
"imageUrl": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ", // conversation 1
"read": "true"
},
{
"fromId": "bb888skjaaasjdfwe2333",
"toId": "qq43922sdkfjsfmmxdfqq",
"messageText": "What kind of pizza do you want?",
"imageUrl": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "EnotMkk8REEd0DHGvUgnd45wBap80E", // conversation 2 - same message id as the 2nd message in Messages.josn with nickname Tom.
"read": "true"
},
{
"fromId": "bb888skjaaasjdfwe2333",
"toId": "qq43922sdkfjsfmmxdfqq",
"messageText": "I like ham & pineapple pizza.",
"imageUrl": "https://i.imgur.com/PJcyle7.jpg",
"message_id": "EnotMkk8REEd0DHGvUgnd45wBap80E", // conversation 2
"read": "true"
}
]
let msg = try decoder.decode([ChatModel].self, from: data)
self.chats = msg
You are storing all chat data contained in Chats.json into chats property.
To filter chats, try replacing second line like below
self.chats = msg.filter { chat in
return chat.chatMessageId == self.messageModel.chatMessageId
}
or, while I don't know why all chats are contained in single json file, it will be better to separate json file into multiple files for each chat like Chat-Tva9d2OJyWHRC1AqEfKjclRwXnlRDQ.json.
I hope this will help.
Your MessageListController didSelectRowAt method should be as simple as this.
//MessageListController
// ...code...
let messageObject = [MessageModel]()
// var chatObject = [ChatModel]()
// ...code...
override func table(_ table: WKInterfaceTable, didSelectRowAt rowIndex: Int) {
//creating a context with the message object at selected row.
var messageContext = messageObject[rowIndex]
// WatchKit's model presentation method. passing the context along.
presentController(withName: "ChatDetailController", context: messageContext)
// Rest of the codes not required in your MessageListController
}
// ...code...
Your MessageListController will be like this.
// ChatDetailController
var chats: [ChatModel] = []
var messageModel: MessageModel? {
didSet {
guard let model = messageModel else { return }
partnerLabel.setText(model.nickname)
}
}
override func awake(withContext context: Any?) {
super.awake(withContext: context)
if let message = context as? MessageModel {
self.messageModel = message
print("Passed context: \(String(describing: messageModel))")
} else {
print("Passed context error: \(String(describing: context))")
}
do {
guard let fileUrl = Bundle.main.url(forResource: "Chats", withExtension: "json") else {
print("File could not be located")
return
}
let data = try Data(contentsOf: fileUrl)
let decoder = JSONDecoder()
//msg holds all chat messages in the file
let msg = try decoder.decode([ChatModel].self, from: data)
// We should filter the chats in the msg array with the selected message content
self.chats = msg.filter({
$0.chatMessageId == messageModel?.chatMessageId
})
// self.chats only holds the chats with chatMessageId
} catch let error {
print(error)
}
DispatchQueue.main.async(execute: {
self.setupTable(chatMessageArray: self.chats as [AnyObject])
})
Hope this will resolve your issue. Happy Coding :)
While using Swift4 and Codable protocols I got the following problem - it looks like there is no way to allow JSONDecoder to skip elements in an array.
For example, I have the following JSON:
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
And a Codable struct:
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
When decoding this json
let decoder = JSONDecoder()
let products = try decoder.decode([GroceryProduct].self, from: json)
Resulting products is empty. Which is to be expected, due to the fact that the second object in JSON has no "points" key, while points is not optional in GroceryProduct struct.
Question is how can I allow JSONDecoder to "skip" invalid object?
One option is to use a wrapper type that attempts to decode a given value; storing nil if unsuccessful:
struct FailableDecodable<Base : Decodable> : Decodable {
let base: Base?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
self.base = try? container.decode(Base.self)
}
}
We can then decode an array of these, with your GroceryProduct filling in the Base placeholder:
import Foundation
let json = """
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
""".data(using: .utf8)!
struct GroceryProduct : Codable {
var name: String
var points: Int
var description: String?
}
let products = try JSONDecoder()
.decode([FailableDecodable<GroceryProduct>].self, from: json)
.compactMap { $0.base } // .flatMap in Swift 4.0
print(products)
// [
// GroceryProduct(
// name: "Banana", points: 200,
// description: Optional("A banana grown in Ecuador.")
// )
// ]
We're then using .compactMap { $0.base } to filter out nil elements (those that threw an error on decoding).
This will create an intermediate array of [FailableDecodable<GroceryProduct>], which shouldn't be an issue; however if you wish to avoid it, you could always create another wrapper type that decodes and unwraps each element from an unkeyed container:
struct FailableCodableArray<Element : Codable> : Codable {
var elements: [Element]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var elements = [Element]()
if let count = container.count {
elements.reserveCapacity(count)
}
while !container.isAtEnd {
if let element = try container
.decode(FailableDecodable<Element>.self).base {
elements.append(element)
}
}
self.elements = elements
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(elements)
}
}
You would then decode as:
let products = try JSONDecoder()
.decode(FailableCodableArray<GroceryProduct>.self, from: json)
.elements
print(products)
// [
// GroceryProduct(
// name: "Banana", points: 200,
// description: Optional("A banana grown in Ecuador.")
// )
// ]
I would create a new type Throwable, which can wrap any type conforming to Decodable:
enum Throwable<T: Decodable>: Decodable {
case success(T)
case failure(Error)
init(from decoder: Decoder) throws {
do {
let decoded = try T(from: decoder)
self = .success(decoded)
} catch let error {
self = .failure(error)
}
}
}
For decoding an array of GroceryProduct (or any other Collection):
let decoder = JSONDecoder()
let throwables = try decoder.decode([Throwable<GroceryProduct>].self, from: json)
let products = throwables.compactMap { $0.value }
where value is a computed property introduced in an extension on Throwable:
extension Throwable {
var value: T? {
switch self {
case .failure(_):
return nil
case .success(let value):
return value
}
}
}
I would opt for using a enum wrapper type (over a Struct) because it may be useful to keep track of the errors that are thrown as well as their indices.
Swift 5
For Swift 5 Consider using the Result enum e.g.
struct Throwable<T: Decodable>: Decodable {
let result: Result<T, Error>
init(from decoder: Decoder) throws {
result = Result(catching: { try T(from: decoder) })
}
}
To unwrap the decoded value use the get() method on the result property:
let products = throwables.compactMap { try? $0.result.get() }
The problem is that when iterating over a container, the container.currentIndex isn’t incremented so you can try to decode again with a different type.
Because the currentIndex is read only, a solution is to increment it yourself successfully decoding a dummy. I took #Hamish solution, and wrote a wrapper with a custom init.
This problem is a current Swift bug: https://bugs.swift.org/browse/SR-5953
The solution posted here is a workaround in one of the comments.
I like this option because I’m parsing a bunch of models the same way on a network client, and I wanted the solution to be local to one of the objects. That is, I still want the others to be discarded.
I explain better in my github https://github.com/phynet/Lossy-array-decode-swift4
import Foundation
let json = """
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
""".data(using: .utf8)!
private struct DummyCodable: Codable {}
struct Groceries: Codable
{
var groceries: [GroceryProduct]
init(from decoder: Decoder) throws {
var groceries = [GroceryProduct]()
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
if let route = try? container.decode(GroceryProduct.self) {
groceries.append(route)
} else {
_ = try? container.decode(DummyCodable.self) // <-- TRICK
}
}
self.groceries = groceries
}
}
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let products = try JSONDecoder().decode(Groceries.self, from: json)
print(products)
There are two options:
Declare all members of the struct as optional whose keys can be missing
struct GroceryProduct: Codable {
var name: String
var points : Int?
var description: String?
}
Write a custom initializer to assign default values in the nil case.
struct GroceryProduct: Codable {
var name: String
var points : Int
var description: String
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
points = try values.decodeIfPresent(Int.self, forKey: .points) ?? 0
description = try values.decodeIfPresent(String.self, forKey: .description) ?? ""
}
}
A solution made possible by Swift 5.1, using the property wrapper:
#propertyWrapper
struct IgnoreFailure<Value: Decodable>: Decodable {
var wrappedValue: [Value] = []
private struct _None: Decodable {}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
while !container.isAtEnd {
if let decoded = try? container.decode(Value.self) {
wrappedValue.append(decoded)
}
else {
// item is silently ignored.
try? container.decode(_None.self)
}
}
}
}
And then the usage:
let json = """
{
"products": [
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
}
""".data(using: .utf8)!
struct GroceryProduct: Decodable {
var name: String
var points: Int
var description: String?
}
struct ProductResponse: Decodable {
#IgnoreFailure
var products: [GroceryProduct]
}
let response = try! JSONDecoder().decode(ProductResponse.self, from: json)
print(response.products) // Only contains banana.
Note: The property wrapper things will only works if the response can be wrapped in a struct (i.e: not a top level array).
In that case, you can still wrap it manually (with a typealias for better readability):
typealias ArrayIgnoringFailure<Value: Decodable> = IgnoreFailure<Value>
let response = try! JSONDecoder().decode(ArrayIgnoringFailure<GroceryProduct>.self, from: json)
print(response.wrappedValue) // Only contains banana.
Ive put #sophy-swicz solution, with some modifications, into an easy to use extension
fileprivate struct DummyCodable: Codable {}
extension UnkeyedDecodingContainer {
public mutating func decodeArray<T>(_ type: T.Type) throws -> [T] where T : Decodable {
var array = [T]()
while !self.isAtEnd {
do {
let item = try self.decode(T.self)
array.append(item)
} catch let error {
print("error: \(error)")
// hack to increment currentIndex
_ = try self.decode(DummyCodable.self)
}
}
return array
}
}
extension KeyedDecodingContainerProtocol {
public func decodeArray<T>(_ type: T.Type, forKey key: Self.Key) throws -> [T] where T : Decodable {
var unkeyedContainer = try self.nestedUnkeyedContainer(forKey: key)
return try unkeyedContainer.decodeArray(type)
}
}
Just call it like this
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.items = try container.decodeArray(ItemType.self, forKey: . items)
}
For the example above:
let json = """
[
{
"name": "Banana",
"points": 200,
"description": "A banana grown in Ecuador."
},
{
"name": "Orange"
}
]
""".data(using: .utf8)!
struct Groceries: Codable
{
var groceries: [GroceryProduct]
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
groceries = try container.decodeArray(GroceryProduct.self)
}
}
struct GroceryProduct: Codable {
var name: String
var points: Int
var description: String?
}
let products = try JSONDecoder().decode(Groceries.self, from: json)
print(products)
Instead, You can also do like this:
struct GroceryProduct: Decodable {
var name: String
var points: Int
var description: String?
}'
and then in while getting it:
'let groceryList = try JSONDecoder().decode(Array<GroceryProduct>.self, from: responseData)'
Unfortunately Swift 4 API doesn't have failable initializer for init(from: Decoder).
Only one solution that I see is implementing custom decoding, giving default value for optional fields and possible filter with needed data:
struct GroceryProduct: Codable {
let name: String
let points: Int?
let description: String
private enum CodingKeys: String, CodingKey {
case name, points, description
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
points = try? container.decode(Int.self, forKey: .points)
description = (try? container.decode(String.self, forKey: .description)) ?? "No description"
}
}
// for test
let dict = [["name": "Banana", "points": 100], ["name": "Nut", "description": "Woof"]]
if let data = try? JSONSerialization.data(withJSONObject: dict, options: []) {
let decoder = JSONDecoder()
let result = try? decoder.decode([GroceryProduct].self, from: data)
print("rawResult: \(result)")
let clearedResult = result?.filter { $0.points != nil }
print("clearedResult: \(clearedResult)")
}
I improved on #Hamish's for the case, that you want this behaviour for all arrays:
private struct OptionalContainer<Base: Codable>: Codable {
let base: Base?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
base = try? container.decode(Base.self)
}
}
private struct OptionalArray<Base: Codable>: Codable {
let result: [Base]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let tmp = try container.decode([OptionalContainer<Base>].self)
result = tmp.compactMap { $0.base }
}
}
extension Array where Element: Codable {
init(from decoder: Decoder) throws {
let optionalArray = try OptionalArray<Element>(from: decoder)
self = optionalArray.result
}
}
Swift 5
Inspired with previous answers I decode inside Result enum extension.
What do you think about it?
extension Result: Decodable where Success: Decodable, Failure == DecodingError {
public init(from decoder: Decoder) throws {
let container: SingleValueDecodingContainer = try decoder.singleValueContainer()
do {
self = .success(try container.decode(Success.self))
} catch {
if let decodingError = error as? DecodingError {
self = .failure(decodingError)
} else {
self = .failure(DecodingError.dataCorrupted(.init(codingPath: [], debugDescription: error.localizedDescription)))
}
}
}
}
Usage
let listResult = try? JSONDecoder().decode([Result<SomeObject, DecodingError>].self, from: ##YOUR DATA##)
let list: [SomeObject] = listResult.compactMap {try? $0.get()}
#Hamish's answer is great. However, you can reduce FailableCodableArray to:
struct FailableCodableArray<Element : Codable> : Codable {
var elements: [Element]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let elements = try container.decode([FailableDecodable<Element>].self)
self.elements = elements.compactMap { $0.wrapped }
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(elements)
}
}
I had a similar issue recently, but slightly different.
struct Person: Codable {
var name: String
var age: Int
var description: String?
var friendnamesArray:[String]?
}
In this case, if one of the element in friendnamesArray is nil, the whole object is nil while decoding.
And the right way to handle this edge case is to declare the string array[String] as array of optional strings[String?] as below,
struct Person: Codable {
var name: String
var age: Int
var description: String?
var friendnamesArray:[String?]?
}
You made the description optional, you should also make the points field optional if there is a chance it could be nil, such as this:
struct GroceryProduct: Codable {
var name: String
var points: Int?
var description: String?
}
Just make sure you safe-unwrap it however you see fit for it's use. I'm guessing nil points == 0 in the actual use case so an example could be:
let products = try JSONDecoder().decode([GroceryProduct].self, from: json)
for product in products {
let name = product.name
let points = product.points ?? 0
let description = product.description ?? ""
ProductView(name, points, description)
}
or in-line:
let products = try JSONDecoder().decode([GroceryProduct].self, from: json)
for product in products {
ProductView(product.name, product.points ?? 0, product.description ?? "")
}
I come up with this KeyedDecodingContainer.safelyDecodeArray that provides a simple interface:
extension KeyedDecodingContainer {
/// The sole purpose of this `EmptyDecodable` is allowing decoder to skip an element that cannot be decoded.
private struct EmptyDecodable: Decodable {}
/// Return successfully decoded elements even if some of the element fails to decode.
func safelyDecodeArray<T: Decodable>(of type: T.Type, forKey key: KeyedDecodingContainer.Key) -> [T] {
guard var container = try? nestedUnkeyedContainer(forKey: key) else {
return []
}
var elements = [T]()
elements.reserveCapacity(container.count ?? 0)
while !container.isAtEnd {
/*
Note:
When decoding an element fails, the decoder does not move on the next element upon failure, so that we can retry the same element again
by other means. However, this behavior potentially keeps `while !container.isAtEnd` looping forever, and Apple does not offer a `.skipFailable`
decoder option yet. As a result, `catch` needs to manually skip the failed element by decoding it into an `EmptyDecodable` that always succeed.
See the Swift ticket https://bugs.swift.org/browse/SR-5953.
*/
do {
elements.append(try container.decode(T.self))
} catch {
if let decodingError = error as? DecodingError {
Logger.error("\(#function): skipping one element: \(decodingError)")
} else {
Logger.error("\(#function): skipping one element: \(error)")
}
_ = try? container.decode(EmptyDecodable.self) // skip the current element by decoding it into an empty `Decodable`
}
}
return elements
}
}
The potentially infinite loop while !container.isAtEnd is a concern, and it's addressed by using EmptyDecodable.
A much simpler attempt:
Why don't you declare points as optional or make the array contain optional elements
let products = [GroceryProduct?]
Features:
Simple use. One line in Decodable instance: let array: CompactDecodableArray<Int>
Is decoded with standard mapping mechanism: JSONDecoder().decode(Model.self, from: data)
skips incorrect elements (returns array with only successful mapped elements)
Details
Xcode 12.1 (12A7403)
Swift 5.3
Solution
class CompactDecodableArray<Element>: Decodable where Element: Decodable {
private(set) var elements = [Element]()
required init(from decoder: Decoder) throws {
guard var unkeyedContainer = try? decoder.unkeyedContainer() else { return }
while !unkeyedContainer.isAtEnd {
if let value = try? unkeyedContainer.decode(Element.self) {
elements.append(value)
} else {
unkeyedContainer.skip()
}
}
}
}
// https://forums.swift.org/t/pitch-unkeyeddecodingcontainer-movenext-to-skip-items-in-deserialization/22151/17
struct Empty: Decodable { }
extension UnkeyedDecodingContainer {
mutating func skip() { _ = try? decode(Empty.self) }
}
Usage
struct Model2: Decodable {
let num: Int
let str: String
}
struct Model: Decodable {
let num: Int
let str: String
let array1: CompactDecodableArray<Int>
let array2: CompactDecodableArray<Int>?
let array4: CompactDecodableArray<Model2>
}
let dictionary: [String : Any] = ["num": 1, "str": "blablabla",
"array1": [1,2,3],
"array3": [1,nil,3],
"array4": [["num": 1, "str": "a"], ["num": 2]]
]
let data = try! JSONSerialization.data(withJSONObject: dictionary)
let object = try JSONDecoder().decode(Model.self, from: data)
print("1. \(object.array1.elements)")
print("2. \(object.array2?.elements)")
print("3. \(object.array4.elements)")
Console
1. [1, 2, 3]
2. nil
3. [__lldb_expr_25.Model2(num: 1, str: "a")]