Parsing Array of dictionaries in Swift using Codable - json

I have a JSON response from an API and I cannot figure out how to convert it into an user object (a single user) using Swift Codable. This is the JSON (with some elements removed for ease of reading):
{
"user": [
{
"key": "id",
"value": "093"
},
{
"key": "name",
"value": "DEV"
},
{
"key": "city",
"value": "LG"
},
{
"key": "country",
"value": "IN"
},
{
"key": "group",
"value": "OPRR"
}
]
}

You could do it in two steps if you want to. First declare a struct for the received json
struct KeyValue: Decodable {
let key: String
let value: String
}
Then decode the json and map the result into a dictionary using the key/value pairs.
do {
let result = try JSONDecoder().decode([String: [KeyValue]].self, from: data)
if let array = result["user"] {
let dict = array.reduce(into: [:]) { $0[$1.key] = $1.value}
Then encode this dictionary into json and back again using a struct for User
struct User: Decodable {
let id: String
let name: String
let group: String
let city: String
let country: String
}
let userData = try JSONEncoder().encode(dict)
let user = try JSONDecoder().decode(User.self, from: userData)
The whole code block then becomes
do {
let decoder = JSONDecoder()
let result = try decoder.decode([String: [KeyValue]].self, from: data)
if let array = result["user"] {
let dict = array.reduce(into: [:]) { $0[$1.key] = $1.value}
let userData = try JSONEncoder().encode(dict)
let user = try decoder.decode(User.self, from: userData)
//...
}
} catch {
print(error)
}
A bit cumbersome but no manual key/property matching is needed.

you could try a struct with like this
struct Model: Codable {
struct User: Codable {
let key: String
let value: String
}
let singleuser: [User]
}

Create a struct with 2 variables key and value
public struct UserModel: Codable {
public struct User: Codable {
public let key: String
public let value: String
}
public let user: [User]
}
After that using JSONDecoder to decode your string.
func decode(payload: String) {
do {
let template = try JSONDecoder().decode(UserModel.self, from: payload.data(using: .utf8)!)
} catch {
print(error)
}
}

Related

JSON decoding fails when response has different types

I have an API response below. The "USER_LIST" response is different based on the value of "DATA_NUM". The problem I have is when the "DATA_NUM" is "0", it returns an empty string AND when "DATA_NUM" is "1", the "USER_LIST" returns both object and an empty string so that I can't decode with a model below. I want to construct a model that's suitable for every case regardless of the value of the "DATA_NUM".
How can I achieve this? Thanks in advance.
API response
// when "DATA_NUM": "0"
{
"RESPONSE": {
"DATA_NUM": "0",
"USER_LIST": ""
}
}
// when "DATA_NUM": "1"
{
"RESPONSE": {
"DATA_NUM": "1",
"USER_LIST": [
{
"USER_NAME": "Jason",
"USER_AGE": "30",
"ID": "12345"
},
""
]
}
}
// when "DATA_NUM": "2"
{
"RESPONSE": {
"DATA_NUM": "2",
"USER_LIST": [
{
"USER_NAME": "Jason",
"USER_AGE": "30",
"ID": "12345"
},
{
"USER_NAME": "Amy",
"USER_AGE": "24",
"ID": "67890"
}
]
}
}
Model
struct UserDataResponse: Codable {
let RESPONSE: UserData?
}
struct UserData: Codable {
let DATA_NUM: String?
let USER_LIST: [UserInfo]?
}
struct UserInfo: Codable {
let USER_NAME: String?
let USER_AGE: String?
let ID: String?
}
Decode
do {
let res: UserDataResponse = try JSONDecoder().decode(UserDataResponse.self, from: data)
guard let userData: UserData = res.RESPONSE else { return }
print("Successfully decoded", userData)
} catch {
print("failed to decode") // failed to decode when "DATA_NUM" is "0" or "1"
}
Here is a solution using a custom init(from:) to handle the strange USER_LIST
struct UserDataResponse: Decodable {
let response : UserData
enum CodingKeys: String, CodingKey {
case response = "RESPONSE"
}
}
struct UserData: Decodable {
let dataNumber: String
let users: [UserInfo]
enum CodingKeys: String, CodingKey {
case dataNumber = "DATA_NUM"
case users = "USER_LIST"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
dataNumber = try container.decode(String.self, forKey: .dataNumber)
if let _ = try? container.decode(String.self, forKey: .users) {
users = []
return
}
var nestedContainer = try container.nestedUnkeyedContainer(forKey: .users)
var temp: [UserInfo] = []
do {
while !nestedContainer.isAtEnd {
let user = try nestedContainer.decode(UserInfo.self)
temp.append(user)
}
} catch {}
self.users = temp
}
}
struct UserInfo: Decodable {
let name: String
let age: String
let id: String
enum CodingKeys: String, CodingKey {
case name = "USER_NAME"
case age = "USER_AGE"
case id = "ID"
}
}
An example (data1,data2,data3 corresponds to the json examples posted in the question)
let decoder = JSONDecoder()
for data in [data1, data2, data3] {
do {
let result = try decoder.decode(UserDataResponse.self, from: data)
print("Response \(result.response.dataNumber)")
print(result.response.users)
} catch {
print(error)
}
}
Output
Response 0
[]
Response 1
[__lldb_expr_93.UserInfo(name: "Jason", age: "30", id: "12345")]
Response 2
[__lldb_expr_93.UserInfo(name: "Jason", age: "30", id: "12345"), __lldb_expr_93.UserInfo(name: "Amy", age: "24", id: "67890")]
Edit with alternative solution for the while loop
In the above code there is a while loop surrounded by a do/catch so that we exit the loop as soon an error is thrown and this works fine since the problematic empty string is the last element in the json array. This solution was chosen since the iterator for the nestedContainer is not advanced to the next element if the decoding fails so just doing the opposite with the do/catch (where the catch clause is empty) inside the loop would lead to an infinite loop.
An alternative solution that do work is to decode the "" in the catch to advance the iterator. I am not sure if this is needed here but the solution becomes a bit more flexible in case the empty string is somewhere else in the array than last.
Alternative loop:
while !nestedContainer.isAtEnd {
do {
let user = try nestedContainer.decode(UserInfo.self)
temp.append(user)
} catch {
_ = try! nestedContainer.decode(String.self)
}
}
You can write this code to resolve this array string issue.
struct UserDataResponse: Codable {
let RESPONSE: UserData?
}
struct UserData: Codable {
let DATA_NUM: String?
let USER_LIST: [UserInfo]?
struct USER_LIST: Codable {
var USER_LIST: CustomMetadataType
}
}
enum CustomMetadataType: Codable {
case array([String])
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .array(container.decode(Array.self))
} catch DecodingError.typeMismatch {
do {
self = try .string(container.decode(String.self))
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(CustomMetadataType.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Encoded payload not of an expected type"))
}
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .array(let array):
try container.encode(array)
case .string(let string):
try container.encode(string)
}
}
}
struct UserInfo: Codable {
let USER_NAME: String?
let USER_AGE: String?
let ID: String?
}

Fail to decode JSON with swift Combine

I'm trying to read this JSON data with Combine but I cannot make it work if I try to read the release field.
The address https://amiiboapi.com/api/ returns something like this:
{
"amiibo": [
{
"amiiboSeries": "Super Smash Bros.",
"character": "Mario",
"gameSeries": "Super Mario",
"head": "00000000",
"image": "https://raw.githubusercontent.com/N3evin/AmiiboAPI/master/images/icon_00000000-00000002.png",
"name": "Mario",
"release": {
"au": "2014-11-29",
"eu": "2014-11-28",
"jp": "2014-12-06",
"na": "2014-11-21"
},
"tail": "00000002",
"type": "Figure"
},
{
"amiiboSeries": "Super Mario Bros.",
"character": "Mario",
"gameSeries": "Super Mario",
"head": "00000000",
"image": "https://raw.githubusercontent.com/N3evin/AmiiboAPI/master/images/icon_00000000-00340102.png",
"name": "Mario",
"release": {
"au": "2015-03-21",
"eu": "2015-03-20",
"jp": "2015-03-12",
"na": "2015-03-20"
},
"tail": "00340102",
"type": "Figure"
}
]
}
I have my model like this:
// MARK: - Amiibo List
struct AmiibosList: Codable {
let amiibo: [Amiibo]
}
// MARK: - Amiibo
struct Amiibo: Codable {
let amiiboSeries: String
let character: String
let gameSeries: String
let head: String
let image: String
let name: String
let release: Release
let tail: String
let type: String
}
// MARK: - Release
struct Release: Codable {
let au : String?
let eu : String?
let jp : String?
let na : String?
}
And I'm trying to fetch the data like this:
guard let url = URL(string: "https://amiiboapi.com/api/") else {
fatalError("Invalid URL")
}
var publisher = URLSession.shared.dataTaskPublisher(for: url)
.receive(on: RunLoop.main)
.map(\.data)
.decode(type: AmiiboList.self, decoder: JSONDecoder())
.sink(receiveCompletion: { completion in
if case .failure(let err) = completion {
print("Failed with error \(err)")
}
}, receiveValue: { value in
print("Received \(value)")
// print(" Received \(value.amiibo[0].release)")
})
If I comment/remove the release from my amiibo struct, everything works. For some reason I cannot retrieve the data with the release dates and I can't figure why.
Do I need to do anything else for nested JSON data?
With the below, you can simply do: response.amiibo[0].release to get the release object. Note that the release object contains Date objects rather than Strings. That should be helpful.
func example(data: Data) throws -> Response {
let data = jsonString.data(using: .utf8)!
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd"
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .formatted(dateFormatter)
return try decoder.decode(Response.self, from: data)
}
struct Response: Codable {
let amiibo: [Amiibo]
}
struct Amiibo: Codable {
let amiiboSeries: String
let character: String
let gameSeries: String
let head: String
let image: URL?
let name: String
let release: Release
let tail: String
let type: String
}
struct Release: Codable {
let au: Date?
let eu: Date?
let jp: Date?
let na: Date?
}

Decode heterogeneous array JSON using Swift decodable

This is the JSON I am trying to decode. The value of objectType decides what object to create.
{
"options": [
{
"objectType": "OptionTypeA",
"label": "optionALabel1",
"value": "optionAValue1"
},
{
"objectType": "OptionTypeB",
"label": "optionBLabel",
"value": "optionBValue"
},
{
"objectType": "OptionTypeA",
"label": "optionALabel2",
"value": "optionAValue2"
}
]
}
Say I have the 2 Option Types defined like so
public protocol OptionType {
var label: String { get }
var value: String { get }
}
struct OptionTypeA: Decodable {
let label: String
let value: String
// and some others...
}
struct OptionTypeB: Decodable {
let label: String
let value: String
// and some others...
}
struct Option: Decodable {
let options: [OptionType]
enum CodingKeys: String, CodingKey {
case options
case label
case value
case objectType
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
var optionsContainer = try values.nestedUnkeyedContainer(forKey: .options)
var options = [OptionType]()
while !optionsContainer.isAtEnd {
let itemContainer = try optionsContainer.nestedContainer(keyedBy: CodingKeys.self)
switch try itemContainer.decode(String.self, forKey: .objectType) {
// What should I do here so that I do not have to manually decode `OptionTypeA` and `OptionTypeB`?
case "OptionTypeA": options.append()
case "OptionTypeB": options.append()
default: fatalError("Unknown type")
}
}
self.options = options
}
}
I know I can then manually decode each key in itemContainer and create the individual option type objects in the switch case. But I do not want to do that. How can I just decode these objects?
A swiftier way than a protocol for the common properties is an enum with associated values.
The Option enum decodes first the objectType – which can even be decoded as an enum – and depending on the value it decodes the different structs.
enum OptionType : String, Decodable {
case a = "OptionTypeA", b = "OptionTypeB"
}
struct Root : Decodable {
let options : [Option]
}
enum Option : Decodable {
private enum CodingKeys : String, CodingKey { case objectType }
case typeA(OptionTypeA)
case typeB(OptionTypeB)
init(from decoder : Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let typeContainer = try decoder.singleValueContainer()
let optionType = try container.decode(OptionType.self, forKey: .objectType)
switch optionType {
case .a: self = .typeA(try typeContainer.decode(OptionTypeA.self))
case .b: self = .typeB(try typeContainer.decode(OptionTypeB.self))
}
}
}
struct OptionTypeA: Decodable {
let label: String
let value: String
// and some others...
}
struct OptionTypeB: Decodable {
let label: String
let value: String
// and some others...
}
let jsonString = """
{
"options": [
{
"objectType": "OptionTypeA",
"label": "optionALabel1",
"value": "optionAValue1"
},
{
"objectType": "OptionTypeB",
"label": "optionBLabel",
"value": "optionBValue"
},
{
"objectType": "OptionTypeA",
"label": "optionALabel2",
"value": "optionAValue2"
}
]
}
"""
let data = Data(jsonString.utf8)
do {
let result = try JSONDecoder().decode(Root.self, from: data)
for option in result.options {
switch option {
case .typeA(let optionTypeA): print(optionTypeA)
case .typeB(let optionTypeB): print(optionTypeB)
}
}
} catch {
print(error)
}

Swift FAILURE: type Mismatch(Swift.Array<Any>)

Is there anyone can help me fix my model? It seems it does not match with the JSON from API Response.
JSON response from postman
{
"error_code": 0,
"data": [
{
"kode": "001",
"name": "BANK INDONESIA PUSAT JAKARTA"
},
{
"kode": "002",
"name": "PT. BANK RAKYAT INDONESIA (Persero) Tbk."
},
{
"kode": "003",
"name": "BANK EKSPOR INDONESIA"
}
],
"msg": "OK"
}
Last Model Edited:
struct ObjectBank: Codable {
let errorCode: Int
let data: [Bank]
let msg: String
enum CodingKeys : String, CodingKey {
case errorCode = "error_code" , data , msg
}
}
struct Bank: Codable {
let kode: String
let name: String
}
Still got error like this
Store model using alamofire
private static func performRequest<T:Decodable>(route:APIRouter,
decoder: JSONDecoder = JSONDecoder(), completion:#escaping
(Result<T>)->Void) -> DataRequest {
// Alamofire.request(route).responseJSON {
// response in
// print(response)
// }
return Alamofire.request(route).responseJSONDecodable (decoder:
decoder){ (response: DataResponse<T>) in
//print(response)
completion(response.result)
}
}
data is an array not dictionary
let data:[Bank]
//
struct ObjectBank: Codable {
let errorCode: Int
let data: [Bank]
let msg: String
enum CodingKeys : String, CodingKey {
case errorCode = "error_code" , data , msg
}
}
struct Bank: Codable {
let kode: String
let name: String
}
//
do {
let dic = try JSONDecoder().decode(ObjectBank.self,data)
}
catch {
print(error)
}
The structure of your response is ok in principle which you can see using the following Playground:
import Cocoa
let jsonData = """
{
"error_code": 0,
"data": [
{
"kode": "001",
"name": "BANK INDONESIA PUSAT JAKARTA"
},
{
"kode": "002",
"name": "PT. BANK RAKYAT INDONESIA (Persero) Tbk."
},
{
"kode": "003",
"name": "BANK EKSPOR INDONESIA"
}
],
"msg": "OK"
}
""".data(using: .utf8)!
struct ObjectBank: Codable {
let errorCode: Int
let data: [Bank]
let msg: String
enum CodingKeys : String, CodingKey {
case errorCode = "error_code" , data , msg
}
}
struct Bank: Codable {
let kode: String
let name: String
}
do {
let banks = try JSONDecoder().decode(ObjectBank.self, from: jsonData)
print(banks)
} catch {
print(error)
}
This will parse without error. Since I do not know AlamoFire very well I have to assume that there is something going wrong with the type of your completion closure. It will "somehow" have to guess that you want to parse ObjectBank in order to make any sense of your response.
Maybe you would have an easier time with responseData?

Decoding nested JSON with Swift 4

JSON =
{
"html_attributions": [],
"results": [
{
"geometry": {},
"name": "Cruise Bar, Restaurant & Events",
"vicinity": "Circular Quay W, Sydney"
},
{}
],
"status": "OK"
}
How do I retrieve name if it is nested within results?
Sam Try this i write sample code in playground using your Json. root.results will give you array of dictionary, you can easily traverse and get your desired name from it.
import UIKit
struct Root: Codable {
let results: [Results]?
private enum CodingKeys: String, CodingKey {
case results = "results"
}
}
struct Results: Codable {
let name: String?
let vicinity: String?
}
let url = Bundle.main.url(forResource: "data", withExtension: "json")
let data = NSData(contentsOf: url!)
do {
let root = try JSONDecoder().decode(Root.self, from: data as! Data)
if let name = root.results?.first?.name {
print(name)
}
} catch let error as NSError {
print(error.description)
}
Here is the json i have used.
{
"results": [{
"name": "Cruise Bar, Restaurant & Events",
"vicinity": "Circular Quay W, Sydney"
}]
}
You can do it like that:
Model:
import Foundation
struct HtmlInitial: Codable {
let results: [Result]?
let status: String
enum CodingKeys: String, CodingKey {
case results, status
}
}
struct Result: Codable {
let name, vicinity: String?
}
extension HtmlInitial {
init(data: Data) throws {
self = try JSONDecoder().decode(HtmlInitial.self, from: data)
}
}
use model Like that :
let url = Bundle.main.url(forResource: "APIResponse", withExtension: "json")!
if let data = try? Data.init(contentsOf: url) ,
let initial = try? HtmlInitial.init(data: data),
let result = initial.results?[0] {
print(result.name)
}