I am trying to parse a json string in which some of the keys are not fixed. There are some keys which are related to error and either it will be error or the result data. Followings are the two examples:
{
"ok": true,
"result": {
"code": "694kyH",
"short_link": "shrtco.de\/694kyH",
"full_short_link": "https:\/\/shrtco.de\/694kyH",
"short_link2": "9qr.de\/694kyH",
"full_short_link2": "https:\/\/9qr.de\/694kyH",
"short_link3": "shiny.link\/694kyH",
"full_short_link3": "https:\/\/shiny.link\/694kyH",
"share_link": "shrtco.de\/share\/694kyH",
"full_share_link": "https:\/\/shrtco.de\/share\/694kyH",
"original_link": "http:\/\/google.com"
}
}
{
"ok": false,
"error_code": 2,
"error": "This is not a valid URL, for more infos see shrtco.de\/docs"
}
How will I parse this JSON. I have tried to build my class like following but it is not working:
struct ShortLinkData: Codable {
let ok: Bool
let result: Result?
let errorCode: Int?
let error: String?
private enum CodingKeys : String, CodingKey { case ok, result, errorCode = "error_code", error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
ok = try container.decode(Bool.self, forKey: .ok)
result = try container.decode(Result.self, forKey: .result)
errorCode = try container.decodeIfPresent(Int.self, forKey: .errorCode)
error = try container.decodeIfPresent(String.self, forKey: .error)
}
}
// MARK: - Result
struct Result: Codable {
let code, shortLink: String
let fullShortLink: String
let shortLink2: String
let fullShortLink2: String
let shortLink3: String
let fullShortLink3: String
let shareLink: String
let fullShareLink: String
let originalLink: String
enum CodingKeys: String, CodingKey {
case code
case shortLink = "short_link"
case fullShortLink = "full_short_link"
case shortLink2 = "short_link2"
case fullShortLink2 = "full_short_link2"
case shortLink3 = "short_link3"
case fullShortLink3 = "full_short_link3"
case shareLink = "share_link"
case fullShareLink = "full_share_link"
case originalLink = "original_link"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
code = try container.decode(String.self, forKey: .code)
shortLink = try container.decode(String.self, forKey: .shortLink)
fullShortLink = try container.decode(String.self, forKey: .fullShortLink)
shortLink2 = try container.decode(String.self, forKey: .shortLink2)
fullShortLink2 = try container.decode(String.self, forKey: .fullShortLink2)
shortLink3 = try container.decode(String.self, forKey: .shortLink3)
fullShortLink3 = try container.decode(String.self, forKey: .fullShortLink3)
shareLink = try container.decode(String.self, forKey: .shareLink)
fullShareLink = try container.decode(String.self, forKey: .fullShareLink)
originalLink = try container.decode(String.self, forKey: .originalLink)
}
}
My parsing code:
let str = String(decoding: data, as: UTF8.self)
print(str)
let shortURL = try? JSONDecoder().decode(ShortLinkData.self, from: data)
return shortURL!
I am always getting nil in shortURL object.
You should split this into several steps in order to avoid to handle all these optionals in your model.
First create a struct that has only those properties that are guaranteed to be there. ok in your case:
struct OKResult: Codable{
let ok: Bool
}
then create one for your error state and one for your success state:
struct ErrorResult: Codable{
let ok: Bool
let errorCode: Int
let error: String
private enum CodingKeys: String, CodingKey{
case ok, errorCode = "error_code", error
}
}
struct ShortLinkData: Codable {
let ok: Bool
let result: Result
}
struct Result: Codable {
let code, shortLink: String
let fullShortLink: String
let shortLink2: String
let fullShortLink2: String
let shortLink3: String
let fullShortLink3: String
let shareLink: String
let fullShareLink: String
let originalLink: String
enum CodingKeys: String, CodingKey {
case code
case shortLink = "short_link"
case fullShortLink = "full_short_link"
case shortLink2 = "short_link2"
case fullShortLink2 = "full_short_link2"
case shortLink3 = "short_link3"
case fullShortLink3 = "full_short_link3"
case shareLink = "share_link"
case fullShareLink = "full_share_link"
case originalLink = "original_link"
}
}
Then you can decode the data:
guard try JSONDecoder().decode(OKResult.self, from: data).ok else{
let errorResponse = try JSONDecoder().decode(ErrorResult.self, from: data)
//handle error scenario
fatalError(errorResponse.error) // or throw custom error or return nil etc...
}
let shortlinkData = try JSONDecoder().decode(ShortLinkData.self, from: data)
Remarks:
Your inits are not necessary.
Never use try? this will hide all errors from you
you would need to wrap this either in a do catch block or make your function throwing and handle errors further up the tree.
Actually there are no optional fields. The server sends two different but distinct JSON strings.
A suitable way to decode both JSON strings is an enum with associated values. It decodes the ok key, then it decodes either the result dictionary or errorCode and error
enum Response : Decodable {
case success(ShortLinkData), failure(Int, String)
private enum CodingKeys : String, CodingKey { case ok, result, errorCode, error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let ok = try container.decode(Bool.self, forKey: .ok)
if ok {
let result = try container.decode(ShortLinkData.self, forKey: .result)
self = .success(result)
} else {
let errorCode = try container.decode(Int.self, forKey: .errorCode)
let error = try container.decode(String.self, forKey: .error)
self = .failure(errorCode, error)
}
}
}
In ShortLinkData the init method and the CodingKeys are redundant if you specify the convertFromSnakeCase key decoding strategy
struct ShortLinkData: Decodable {
let code, shortLink: String
let fullShortLink: String
let shortLink2, fullShortLink2: String
let shortLink3, fullShortLink3: String
let shareLink, fullShareLink: String
let originalLink: String
}
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Response.self, from: data)
switch result {
case .success(let linkData): print(linkData)
case .failure(let code, let message): print("An error occurred with code \(code) and message \(message)")
}
} catch {
print(error)
}
Related
Saving data in defaults:
let encoder = JSONEncoder()
if let encoded = try? encoder.encode(user) {
let defaults = UserDefaults.standard
defaults.set(encoded, forKey: DefaultKeys.user)
defaults.synchronize()
}
fetching data but getting nil keys of struct.
let defaults = UserDefaults.standard
if let savedPerson = defaults.object(forKey: DefaultKeys.user) as? Data {
let decoder = JSONDecoder()
if let loadedPerson = try? decoder.decode(UserModel.self, from: savedPerson) {
return loadedPerson
}
}
I tried to google the approaches everyone suggest the same to save custom object in userdefaults, but the approach is not working for me.Given below is my struct model which conform Codable as well.
struct UserModel : Codable {
var userDetails : UserDetail?
var status : Bool?
var message : String?
enum CodingKeys: String, CodingKey {
case userDetails = "userDetails"
case status = "status"
case message = "message"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
userDetails = try UserDetail(from: decoder)
status = try values.decodeIfPresent(Bool.self, forKey: .status)
message = try values.decodeIfPresent(String.self, forKey: .message)
}
}
{
"type": "Success",
"message": "",
"fitting": {
"fitterID": "96ba096c-f0aa-11e7-a67a-76478bc72e4d",
"fitID": "09d399c0-7d74-4578-a138-5f4b02ba2e80",
"leftNotesJSON": "[{\"class\":\"FitNote\",\"text\":\"Saddle Down\",\"leftfoot\":false},{\"class\":\"FitNote\",\"text\":\"Saddle Down\",\"leftfoot\":false},{\"class\":\"FootBottomNote\",\"leftfoot\":false}]",
"rightNotesJSON": "[{\"s3Bucket\":\"8190ba10-d310-11e3-9c1a-0800200c9a66\",\"angle\":0,\"leftfoot\":false,\"shoulderAngle\":0,\"hipAngle\":0,\"s3Key\":\"FD0F5AE6-8193-4980-AD11-C42FEF064B8B\",\"class\":\"AngleNote\",\"kneeAngle\":0}]"
}
}
The json in your question is not valid. I'll be using the following json in my answer:
let json = """
{
"type": "Success",
"message": "",
"fitting": {
"fitterID": "96ba096c-f0aa-11e7-a67a-76478bc72e4d",
"fitID": "09d399c0-7d74-4578-a138-5f4b02ba2e80",
"leftNotesJSON": [{"class":"FitNote","text":"Saddle Down","leftfoot":false},{"class":"FitNote","text":"Saddle Down","leftfoot":false},{"class":"FootBottomNote","leftfoot":false}],
"rightNotesJSON": [{"s3Bucket":"8190ba10-d310-11e3-9c1a-0800200c9a66","angle":0,"leftfoot":false,"shoulderAngle":0,"hipAngle":0,"s3Key":"FD0F5AE6-8193-4980-AD11-C42FEF064B8B","class":"AngleNote","kneeAngle":0}]
}
}
"""
Let's define decodable structs:
struct Response: Codable {
let type, message: String
let fitting: Fitting
}
struct Fitting: Codable {
let fitterID, fitID: String
let leftNotesJSON: [LeftNotesJSON]
let rightNotesJSON: [RightNotesJSON]
}
struct LeftNotesJSON: Codable {
let leftNotesJSONClass: String
let text: String?
let leftfoot: Bool
//Define the coding keys since the json contains "class" as a key
enum CodingKeys: String, CodingKey {
case leftNotesJSONClass = "class"
case text, leftfoot
}
}
struct RightNotesJSON: Codable {
let s3Bucket: String
let angle: Int
let leftfoot: Bool
let shoulderAngle, hipAngle: Int
let s3Key, rightNotesJSONClass: String
let kneeAngle: Int
//Define the coding keys since the json contains "class" as a key
enum CodingKeys: String, CodingKey {
case s3Bucket, angle, leftfoot, shoulderAngle, hipAngle, s3Key
case rightNotesJSONClass = "class"
case kneeAngle
}
}
Let's get the data out of the json:
guard let data = json.data(using: .utf8) else {
fatalError("Couldn't get data from json")
}
And then decode it
do {
let response = try JSONDecoder().decode(Response.self, from: data)
//Here and now you can use the properties of the response
print(response.type)
print(response.message)
print(response.fitting.fitID)
print(response.fitting.fitterID)
print(response.fitting.leftNotesJSON.map {$0.leftfoot})
print(response.fitting.rightNotesJSON.map{$0.kneeAngle})
} catch {
print(error)
}
First, you may have to create models, which confirms to Codable Protocol. You can use any online tools for formatting JSON and creating models. In given code response Model will get parsed data.
import Foundation
struct Base : Codable {
let type : String?
let message : String?
let fitting : Fitting?
enum CodingKeys: String, CodingKey {
case type = "type"
case message = "message"
case fitting = "fitting"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decodeIfPresent(String.self, forKey: .type)
message = try values.decodeIfPresent(String.self, forKey: .message)
fitting = try values.decodeIfPresent(Fitting.self, forKey: .fitting)
}
}
import Foundation
struct Fitting : Codable {
let fitterID : String?
let fitID : String?
let leftNotesJSON : String?
let rightNotesJSON : String?
enum CodingKeys: String, CodingKey {
case fitterID = "fitterID"
case fitID = "fitID"
case leftNotesJSON = "leftNotesJSON"
case rightNotesJSON = "rightNotesJSON"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
fitterID = try values.decodeIfPresent(String.self, forKey: .fitterID)
fitID = try values.decodeIfPresent(String.self, forKey: .fitID)
leftNotesJSON = try values.decodeIfPresent(String.self, forKey: .leftNotesJSON)
rightNotesJSON = try values.decodeIfPresent(String.self, forKey: .rightNotesJSON)
}
}
let task = URLSession.shared.dataTask(with: <YOUR URL>) { (data, response, error) in
if let data = data {
let jsonDecoder = JSONDecoder()
let responseModel = try jsonDecoder.decode(Base.self, from: data)
}
}
task.resume()
I define a model like this:
struct DMTest: Codable {
var uid: Int
var name: String?
}
and do the model decode like this:
let jsonStr = "{\"uid\":123,\"name\":\"haha\"}"
let jsonData = jsonStr.data(using: .utf8)!
do {
let decoder = JSONDecoder()
let result = try decoder.decode(DMTest.self, from:jsonData)
XCGLogger.debug("result = \(result)")
}catch {
XCGLogger.debug("error")
}
When jsonStr is like below, it works well:
{"uid":123,"name":"haha"}
when jsonStr is like below, it will throw a exception:
{"uid":"123","name":"haha"}
It means that if the type of the "uid" not adapter, it can't be decode. But some times the framework of server is weak type, it may give me dirty data like this, How can I adjust the type?
For example: I define Int in the model, if the server give some data of String type and I can covert it to Int, just decode to Int otherwise throw ecxeption.
You can define a custom parsing solution:
struct DMTest: Codable {
enum CodingKeys: String, CodingKey {
case uid, name
}
var uid: Int
var name: String?
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let stringUID = try? container.decode(String.self, forKey: .uid), let intUID = Int(stringUID) {
uid = intUID
} else if let intUID = try? container.decode(Int.self, forKey: .uid) {
uid = intUID
} else {
uid = 0 // or throw error
}
name = try container.decodeIfPresent(String.self, forKey: .name)
}
}
Let's say we have this URL, I'm using CodableAlamofire to fetch and parse the JSON response.
For the above URL response I've created following Codable classes.
struct CoinList: Codable {
let raw: Raw
let display: Display
enum CodingKeys: String, CodingKey {
case raw = "RAW"
case display = "DISPLAY"
}
}
struct Display: Codable {
let usd: [String: Usd]
enum CodingKeys: String, CodingKey {
case usd = "USD"
}
}
struct Raw: Codable {
let usd: [String: Usd]
enum CodingKeys: String, CodingKey {
case usd = "USD"
}
}
struct Usd: Codable {
let type, market, fromsymbol, tosymbol: String
let flags: String
let price: Double
let lastupdate: Int
let lastvolume, lastvolumeto: Double
let lasttradeid: String
let volumeday, volumedayto, volume24Hour, volume24Hourto: Double
let openday, highday, lowday, open24Hour: Double
let high24Hour, low24Hour: Double
let lastmarket: String
let change24Hour, changepct24Hour, changeday, changepctday: Double
let supply, mktcap, totalvolume24H, totalvolume24Hto: Double
enum CodingKeys: String, CodingKey {
case type = "TYPE"
case market = "MARKET"
case fromsymbol = "FROMSYMBOL"
case tosymbol = "TOSYMBOL"
case flags = "FLAGS"
case price = "PRICE"
case lastupdate = "LASTUPDATE"
case lastvolume = "LASTVOLUME"
case lastvolumeto = "LASTVOLUMETO"
case lasttradeid = "LASTTRADEID"
case volumeday = "VOLUMEDAY"
case volumedayto = "VOLUMEDAYTO"
case volume24Hour = "VOLUME24HOUR"
case volume24Hourto = "VOLUME24HOURTO"
case openday = "OPENDAY"
case highday = "HIGHDAY"
case lowday = "LOWDAY"
case open24Hour = "OPEN24HOUR"
case high24Hour = "HIGH24HOUR"
case low24Hour = "LOW24HOUR"
case lastmarket = "LASTMARKET"
case change24Hour = "CHANGE24HOUR"
case changepct24Hour = "CHANGEPCT24HOUR"
case changeday = "CHANGEDAY"
case changepctday = "CHANGEPCTDAY"
case supply = "SUPPLY"
case mktcap = "MKTCAP"
case totalvolume24H = "TOTALVOLUME24H"
case totalvolume24Hto = "TOTALVOLUME24HTO"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
type = try values.decode(String.self, forKey: .type)
market = try values.decode(String.self, forKey: .market)
fromsymbol = try values.decode(String.self, forKey: .fromsymbol)
tosymbol = try values.decode(String.self, forKey: .tosymbol)
flags = try values.decode(String.self, forKey: .flags)
price = try values.decode(Double.self, forKey: .price)
lastvolume = try values.decode(Double.self, forKey: .lastvolume)
lastvolumeto = try values.decode(Double.self, forKey: .lastvolumeto)
lastupdate = try values.decode(Int.self, forKey: .lastupdate)
if let value = try? values.decode(Int.self, forKey: .lasttradeid) {
lasttradeid = String(value)
} else {
lasttradeid = try values.decode(String.self, forKey: .lasttradeid)
}
volumeday = try values.decode(Double.self, forKey: .volumeday)
volumedayto = try values.decode(Double.self, forKey: .volumedayto)
volume24Hour = try values.decode(Double.self, forKey: .volume24Hour)
volume24Hourto = try values.decode(Double.self, forKey: .volume24Hourto)
openday = try values.decode(Double.self, forKey: .openday)
highday = try values.decode(Double.self, forKey: .highday)
lowday = try values.decode(Double.self, forKey: .lowday)
open24Hour = try values.decode(Double.self, forKey: .open24Hour)
high24Hour = try values.decode(Double.self, forKey: .high24Hour)
low24Hour = try values.decode(Double.self, forKey: .low24Hour)
lastmarket = try values.decode(String.self, forKey: .lastmarket)
change24Hour = try values.decode(Double.self, forKey: .change24Hour)
changepct24Hour = try values.decode(Double.self, forKey: .changepct24Hour)
changeday = try values.decode(Double.self, forKey: .changeday)
changepctday = try values.decode(Double.self, forKey: .changepctday)
supply = try values.decode(Double.self, forKey: .supply)
mktcap = try values.decode(Double.self, forKey: .mktcap)
totalvolume24H = try values.decode(Double.self, forKey: .totalvolume24H)
totalvolume24Hto = try values.decode(Double.self, forKey: .totalvolume24Hto)
}
}
After successful response, I'm unable to parse the JSON, I studied a lot on nested JSON parsing with Swift Codable but still unable to get success.
Please help me to parse the above JSON response with nested JSON structure, like say Display and Raw object have all properties of Usd.
I think there is some minor mistake I'm doing.
Any help will be appreciated.
UPDATE
I've created JSON file for the response and parsing it,
if let path = Bundle.main.path(forResource: "test", ofType: "json") {
do {
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let result = try JSONDecoder().decode(CoinList.self, from: data)
print(result)
} catch {
print(error.localizedDescription)
}
}
Error is this,
JSON structure is like this,
Please find my my web api call,
Alamofire.request(TickerRouter.PriceMultiFull(params: params))
.validate(statusCode: 200..<300)
.responseString { responseData in
let data = responseData.value?.replacingOccurrences(of: "\\/", with: "/").data(using: .utf8)
if responseData.error == nil {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ AnyKey(stringValue: $0.last!.stringValue.lowercased())!})
decoder.dateDecodingStrategy = .secondsSince1970
let result = try? decoder.decode(CoinList.self, from: data!)
success(result!)
} else {
let msg = "Something went wrong. Please try again later"
failure(msg)
}
}
result is nil here, it is working with local json file. :(
At first glance you will notice that the member types in the USD dictionary for raw and display are widely different, so a single struct for both doesn't work.
The root object is (the String keys are the BTH and XRP symbols)
struct CoinList: Codable {
let raw: [String: Raw]
let display: [String: Display]
}
The Raw and Display structs contain the usd key and the appropriate struct
struct Raw: Codable {
let usd: USDRaw
}
struct Display: Codable {
let usd: USDDisplay
}
The USDRaw and USDDisplay structs contain all data, lastupdate in USDRaw will be decoded as Date
struct USDRaw: Codable {
let type, market, flags, fromsymbol, tosymbol: String
let price : Double
let lastupdate: Date
let lastvolume, lastvolumeto: Double
let lasttradeid: String
let volumeday, volumedayto, volume24hour, volume24hourto: Double
let openday, highday, lowday, open24hour: Double
let high24hour, low24hour: Double
let lastmarket: String
let change24hour, changepct24hour, changeday, changepctday: Double
let supply, mktcap, totalvolume24h, totalvolume24hto: Double
}
struct USDDisplay: Codable {
let fromsymbol, tosymbol, market, price, lastupdate: String
let lastvolume, lastvolumeto, lasttradeid, volumeday, volumedayto, volume24hour, volume24hourto : String
let openday, highday, lowday, open24hour, high24hour, low24hour, lastmarket: String
let change24hour, changepct24hour, changeday, changepctday: String
let supply, mktcap, totalvolume24h, totalvolume24hto: String
}
To get rid of specifying all CodingKeys and make the keys lowercased create a helper struct (stolen from the documentation)
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
Pass the custom keyDecodingStrategy and a suitable dateDecodingStrategy to the decoder
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ AnyKey(stringValue: $0.last!.stringValue.lowercased())!})
decoder.dateDecodingStrategy = .secondsSince1970
let coinList = try decoder.decode(CoinList.self, from: data)
print(coinList)
I have the following models:
struct Shop: Decodable {
let id: Int
let name: String
let products: [Product]
private enum CodingKeys: String, CodingKey {
case id
case name
case products
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: .id)
self.name = try container.decode(String.self, forKey: .name)
self.products = try container.decode([Product].self, forKey: .products)
}
}
struct Product: Decodable {
let id: Int
let title: String
let productImageURL: String
private(set) var productDetails: ProductDetails
private enum CodingKeys: String, CodingKey {
case id
case title
case productImageList
case productImageDetails
case productDetails
}
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.productDetails = try container.decode(ProductDetails.self, forKey: .productDetails)
guard let serializationType = decoder.userInfo[.serializationType] as? SerializationGenericType else {
throw NetworkError.modelParsingError(message: "Product model missing serialization type")
}
switch serializationType {
case .list:
self.productImageURL = try container.decode(String.self, forKey: .productImageList)
case .details:
self.productImageURL = try container.decode(String.self, forKey: .productImageList)
self.productDetails = try container.decode(ProductDetails.self, forKey: .productDetails)
}
}
}
Of course, the models are much more complex, but this is enough to explain my problem :)
Also, please don't mind the coding keys and the image URL, it's just to show that for different serialization type it initialized differently.
When getting the product details I have:
let decoder = JSONDecoder()
decoder.userInfo[.serializationType] = SerializationGenericType.details
let product = try decoder.decode(Product.self, from: data)
But when I try to pass the serialization type when initializing the Shop in the user info of the JsonDecoder like this:
let decoder = JSONDecoder()
decoder.userInfo[.serializationType] = SerializationGenericType.list
let shop = try decoder.decode(Shop.self, from: data)
In the Product initializer the user info is empty.
My question is how to propagate the serialization type from the Shop to the Product so I can properly decode it using the correct serialization type?