Swift Decode Array of Dictionaries inside String using Decodable Protocol - ios

The JSON response is:-
{
"id" = "1"
"message" = "SUCCESS"
"data" = "[{\"name\":"FirstName",\"office_id\":1111,\"days_name\":\"Mon\"},
{\"name\":"SecondName:,\"office_id\":1112,\"days_name\":\"Tue\"}]"
}
I don't seems to understand how to approach decoding "data", In data model shall the data be declared as String? and I have been trying to figure out but don't seem to get any clue and stuck here for a while, if anyone can please shed some light to it, it would help a lot. The only problem I am facing is how to deal with "" double quotes wrapped around data array as shown in above response.
Data model and URLSession code is below:
struct Root : Codable {
let id : String?
let message : String?
let data : String?
enum CodingKeys: String, CodingKey {
case id = "id"
case message = "message"
case data = "data"
}
}
struct insideData: Codable {
let name: String?
let officeId : Int?
let daysName: String?
enum CodingKeys: String, CodingKey {
case name = "name"
case officeId = "office_id"
case daysName = "days_name"
}
}
URLSession.shared.dataTask(with: url!) { (responseData, httpUrlResponse , error) in
if(error == nil && responseData != nil && responseData?.count != 0){
let decoder = JSONDecoder()
do{
let result = try decoder.decode(Root.self, from: responseData!)
print(result.data!)
}
catch let error {
debugPrint("Error occured while decoding = \(error.localizedDescription)")
}
}
}.resume()
I save result.data! in a new variable and convert it to data and again use JSONDecoder but now with insideData.self struct but don't get desired output, not getting mapped with keys inside insideData struct.
I am just getting started with learning networking in swift so please pardon me for silly mistakes.

data value is JSON with JSON, ie it's a JSONString.
A way to parse it is to parse again the JSON String. To do so, you need to override init(from decoder:) of Root.
Let's first fix your JSON which isn't valid, to be able to use it in Playgrounds.
let jsonString = #"""
{
"id": "1",
"message": "SUCCESS",
"data": "[{\"name\":\"FirstName\",\"office_id\":1111,\"days_name\":\"Mon\"}, {\"name\":\"SecondName:\", \"office_id\":1112,\"days_name\":\"Tue\"}]"
}
"""#
Then, change data: let data : [InsideData]
You could then:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(String.self, forKey: .id)
self.message = try container.decodeIfPresent(String.self, forKey: .message)
guard let dataString = try container.decodeIfPresent(String.self, forKey: .data) else {
self.data = []
return
}
self.data = try JSONDecoder().decode([InsideData].self, from: Data(dataString.utf8))
}
If you don't like creating a new decoder, you can pass one in userInfo of the decoder:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decodeIfPresent(String.self, forKey: .id)
self.message = try container.decodeIfPresent(String.self, forKey: .message)
guard let dataString = try container.decodeIfPresent(String.self, forKey: .data) else {
self.data = []
return
}
guard let insideDecoder = decoder.userInfo[CodingUserInfoKey(rawValue: "InsideDecoder")!] as? JSONDecoder else {
self.data = []
return
}
self.data = try insideDecoder.decode([InsideData].self, from: Data(dataString.utf8))
}
Then the root decoding:
let decoder = JSONDecoder()
let insideDecoder = JSONDecoder()
decoder.userInfo = [CodingUserInfoKey(rawValue: "InsideDecoder")!: insideDecoder]
do {
let root = try decoder.decode(Root.self, from: Data(jsonString.utf8))
print(root)
} catch {
print("Error: \(error)")
}

The provided JSON looks to be invalid.
In json you don't use the = sign but the : sign between a key and value.
You are missing , behind each value.
There is a typo behind SecondName :, should be ",
It's weird to have quotes around your data array. It can be easier decoded when you have them removed.
Suggested JSON changes:
{
"id": "1",
"message": "SUCCESS",
"data": [
{"name":"FirstName","office_id":1111,"days_name":"Mon"},
{"name":"SecondName","office_id":1112,"days_name":"Tue"}
]
}
I've tested decoding this json in Playground and it seems to work:
struct DataEntity: Decodable {
let name: String
let officeId: Int
let daysName: String
enum CodingKeys: String, CodingKey {
case name = "name"
case officeId = "office_id"
case daysName = "days_name"
}
}
struct RootEntity: Decodable {
let id: String
let message: String
let data: [DataEntity]
}
struct Mapper {
func map(json: String) -> RootEntity {
guard let rootJsonData = json.data(using: .utf8) else {
fatalError("Couldn't convert json string to data")
}
do {
let rootEntity = try JSONDecoder().decode(RootEntity.self, from: rootJsonData)
return rootEntity
} catch let error {
fatalError(error.localizedDescription)
}
}
}
let jsonToDecode = """
{
"id": "1",
"message": "SUCCESS",
"data": [
{"name":"FirstName","office_id":1111,"days_name":"Mon"},
{"name":"SecondName","office_id":1112,"days_name":"Tue"}
]
}
"""
let rootEntity = Mapper().map(json: jsonToDecode)
print(rootEntity)
Print output:
RootEntity(id: "1", message: "SUCCESS", data: [DataEntity(name: "FirstName", officeId: 1111, daysName: "Mon"), DataEntity(name: "SecondName", officeId: 1112, daysName: "Tue")])

Related

iOS How can I parse JSON codable array with Alamofire

I'm trying data parsing from server with Alamofire.
(I've been trying for 2 days and it's failing.)
How can I get Json array Codable type with Alamofire?...
API :
[
{
"name": "John Doe",
"email": "johndoe#gmail.com",
"type": "Lattee",
"size": "medium"
},
{
"name": "Doe",
"email": "doe#gmail.com",
"type": "Lattee",
"size": "small"
}
]
now this is my code
in Model.swift
struct OrderList : Codable{
var list : [Order]
}
enum coffeeType: String, Codable{
case cappuccino
case lattee
case espressino
case cortado
}
enum coffeeSize: String, Codable{
case small
case medium
case large
enum CodingKeys: String, CodingKey {
case small = "s"
case medium = "m"
case large = "l"
}
}
struct Order: Codable {
let email: String!
let name : String!
let size : coffeeSize!
let type : coffeeType!
enum CodingKeys: String, CodingKey{
case name = "Name"
case email = "Email"
case type = "Type"
case size = "Size"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
email = try values.decodeIfPresent(String.self, forKey: .email) ?? ""
name = try values.decodeIfPresent(String.self, forKey: .name) ?? "Guest"
size = try values.decodeIfPresent(coffeeSize.self, forKey: .size) ?? .small
type = try values.decodeIfPresent(coffeeType.self, forKey: .type) ?? .lattee
}
}
struct Resource<T: Codable> {
let url : URL
var httpMethod: HTTPMethod = .get
}
I have defined it in various formats such as responseData, responseJSON, and responseCodable, but I keep getting nil or something is missing.
I know how to parse with responseJSON. but I want trying to parse by applying Codable...
it's too difficult.
---- data parsing ---
func load<T>(resource: Resource<T>, completion: #escaping (Result<T, NetworkError>) -> Void) {
let call = AF.request(myurl,method: resource.httpMethod, parameters: nil).responseJSON{ response in
switch response.result {
case .success(let data):
if let JSON = response.value {
do{
let dataJson = try JSONSerialization.data(withJSONObject: JSON, options: [])
let getInstanceData = try JSONDecoder().decode(T.self, from: dataJson)
print(getInstanceData)
completion(.success(getInstanceData))
}catch{
print(error)
}
}
case .failure(_):
break
}
}
}
Since the API returns this payload:
[ { "name": "John Doe", "email": "johndoe#gmail.com", "type": "Lattee", "size": "medium" }, { "name": "Doe", "email": "doe#gmail.com", "type": "Lattee", "size": "small" } ]
these are keys that your code needs to handle:
name, email, type, size
Therefore the Order struct should be:
struct Order: Codable {
let email: String
let name : String
let size : CoffeeSize
let type : CoffeeType
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
email = try values.decodeIfPresent(String.self, forKey: .email) ?? ""
name = try values.decodeIfPresent(String.self, forKey: .name) ?? "Guest"
size = try values.decode(CoffeeSize.self, forKey: .size)
type = try values.decode(CoffeeType.self, forKey: .type)
}
}
enum CoffeeType: String, Codable {
case cappuccino
case latte
case espressino
case cortado
init(from decoder: Decoder) throws {
let label = try decoder.singleValueContainer().decode(String.self)
let lowercaseLabel = label.lowercased()
self = CoffeeType(rawValue: lowercaseLabel) ?? .latte
}
}
enum CoffeeSize: String, Codable {
case small
case medium
case large
}
since there is no list key in the json structure therefore you don't need
struct OrderList. In order to get a list of orders you can simply call
load(resource<[Order]>) { result in
// handle the response but now you will get the list of orders
}
Alternatively, I created a playground to load a json file (data.json) locally and parse it into objects you can have a look at the solution below
func readJsonFile(filename: String) -> String {
guard let fileUrl = Bundle.main.url(forResource: filename, withExtension: "json") else { fatalError() }
guard let jsonData = try? String(contentsOf: fileUrl) else {
return ""
}
return jsonData
}
enum CoffeeType: String, Codable {
case cappuccino
case latte
case espressino
case cortado
init(from decoder: Decoder) throws {
let label = try decoder.singleValueContainer().decode(String.self)
let lowercaseLabel = label.lowercased()
self = CoffeeType(rawValue: lowercaseLabel) ?? .latte
}
}
enum CoffeeSize: String, Codable {
case small
case medium
case large
}
struct Order: Codable {
let email: String
let name : String
let size : CoffeeSize
let type : CoffeeType
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
email = try values.decodeIfPresent(String.self, forKey: .email) ?? ""
name = try values.decodeIfPresent(String.self, forKey: .name) ?? "Guest"
size = try values.decode(CoffeeSize.self, forKey: .size)
type = try values.decode(CoffeeType.self, forKey: .type)
}
}
func parseJsonFile() {
let jsonStr = readJsonFile(filename: "data")
let jsonData: Data = Data(jsonStr.utf8)
let decoder = JSONDecoder()
do {
let orders = try decoder.decode([Order].self, from: jsonData)
orders.forEach {
print("\($0.name)" + " - " + "\($0.type.rawValue)" + " - " + "\($0.size.rawValue)")
}
} catch {
print(error.localizedDescription)
}
}
parseJsonFile()
Result:
John Doe - latte - medium
Doe - latte - small
The problem with your code is case-sensitive properties.
If the API returns these keys Name, Type, Email, Size, Type then the Codable object should have the coding Keys to handle keys from API
struct Order: Codable {
let email: String
let name : String
let size : CoffeeSize
let type : CoffeeType
enum CodingKeys: String, CodingKey{
case name = "Name"
case email = "Email"
case type = "Type"
case size = "Size"
}
}
In terms of unwrapping properties, it will depend on the contract between your code and API. If you're pretty sure API will always return size,type, name but email is nullable. Then you should use the method decodeIfPresent allows the value of the parsing key is nullable
If you're certain about name, the value is always not null, then you can use decode normally
and your Order struct will become
struct Order: Codable {
let email: String?
let name : String
let size : CoffeeSize
let type : CoffeeType
enum CodingKeys: String, CodingKey{
case name = "Name"
case email = "Email"
case type = "Type"
case size = "Size"
}
}

Swift: Decode JSON response and store nested JSON as String or JSON

Given the following JSON from a network request; If you wanted to decode this into a Swift object that coforms to Codable, but you wanted to retain the nested JSON that is the value for the key configuration_payload, how could you do it?
{
"registration": {
"id": "0000-0000-0000-0000-000",
"device_type": "device",
"state": "provisioning",
"thing_uuid": 999999999,
"discovery_timeout": 10,
"installation_timeout": 90,
"configuration_payload":
{
"title": "Some Title",
"url": "https://www.someurl.com/",
"category": "test",
"views": 9999
}
}
}
Using the following Swift struct, I want to be able to grab the configuration_payload as a String.
public struct Registration: Codable {
public enum State: String, Codable {
case provisioning, provisioned
}
public let id, deviceType: String
public let state: State
public let error: String?
public let thingUUID: Int?
public let discoveryTimeout, installationTimeout: Int
public let configurationPayload: String?
}
As far as I can tell, the JSONDecoder in Swift, sees the value for configuration_payload as nested JSON and wants to decode it into it's own object. To add to confusion, configuration_payload is not always going to return the same JSON structure, it will vary, so I can not create a Swift struct that I can expect and simply JSON encode it again when needed. I need to be able to store the value as a String to account for variations in the JSON under the configuration_payload key.
As others have already said, you cannot just keep a part without decoding. However, decoding unknown data is trivial:
enum RawJsonValue {
case boolean(Bool)
case number(Double)
case string(String)
case array([RawJsonValue?])
case object([String: RawJsonValue])
}
extension RawJsonValue: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let boolValue = try? container.decode(Bool.self) {
self = .boolean(boolValue)
} else if let numberValue = try? container.decode(Double.self) {
self = .number(numberValue)
} else if let stringValue = try? container.decode(String.self) {
self = .string(stringValue)
} else if let arrayValue = try? container.decode([RawJsonValue?].self) {
self = .array(arrayValue)
} else {
let objectValue = try container.decode([String: RawJsonValue].self)
self = .object(objectValue)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .boolean(let boolValue):
try container.encode(boolValue)
case .number(let numberValue):
try container.encode(numberValue)
case .string(let stringValue):
try container.encode(stringValue)
case .array(let arrayValue):
try container.encode(arrayValue)
case .object(let objectValue):
try container.encode(objectValue)
}
}
}
Now we can safely decode and convert to JSON string if needed:
struct Registration: Codable {
public enum State: String, Codable {
case provisioning, provisioned
}
let id, deviceType: String
let state: State
let error: String?
let thingUUID: Int?
let discoveryTimeout, installationTimeout: Int
let configurationPayload: RawJsonValue?
}
let jsonData = """
{
"id": "0000-0000-0000-0000-000",
"device_type": "device",
"state": "provisioning",
"thing_uuid": 999999999,
"discovery_timeout": 10,
"installation_timeout": 90,
"configuration_payload":
{
"title": "Some Title",
"url": "https://www.someurl.com/",
"category": "test",
"views": 9999
}
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let registration = try! decoder.decode(Registration.self, from: jsonData)
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let payloadString = String(data: try! encoder.encode(registration.configurationPayload), encoding: .utf8)!
print(payloadString) // {"title":"Some Title","views":9999,"url":"https:\/\/www.someurl.com\/","category":"test"}
The only problem I can see is potential loss of precision when decoding decimal numbers, which is a known problem with Foundation JSON decoder.
Also, some null values could be also removed. This could be fixed by decoding object manually by iterating keys and having a special null type.
You can achieve decoding of a JSON object to [String: Any] by using a third party library like AnyCodable.
Your Registration struct will look like this:
public struct Registration: Codable {
public enum State: String, Codable {
case provisioning, provisioned
}
public let id, deviceType: String
public let state: State
public let error: String?
public let thingUUID: Int?
public let discoveryTimeout, installationTimeout: Int
public let configurationPayload: [String: AnyCodable]?
}
and then you can convert [String: AnyCodable] type to [String: Any] or even to String:
let jsonString = """
{
"id": "0000-0000-0000-0000-000",
"device_type": "device",
"state": "provisioning",
"thing_uuid": 999999999,
"discovery_timeout": 10,
"installation_timeout": 90,
"configuration_payload":
{
"title": "Some Title",
"url": "https://www.someurl.com/",
"category": "test",
"views": 9999
}
}
"""
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
let registration = try decoder.decode(Registration.self, from: Data(jsonString.utf8))
// to [String: Any]
let dictionary = registration.configurationPayload?.mapValues { $0.value }
// to String
if let configurationPayload = registration.configurationPayload {
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
let data = try encoder.encode(configurationPayload)
let string = String(decoding: data, as: UTF8.self)
print(string)
}
} catch {
print(error)
}
One (more limited than you probably want) way would be to make sure that Value part in configuration_payload JSON is a known Codable single type (String) instead of Any which can produce multiple types (String, Int, Double etc.).
I was trying to make it work with [String: Any] for the configuration_payload, the problem is Any does NOT conform to Codable.
Then I tried with [String: String] for configuration_payload and was able to make it work like following.
public struct Registration: Codable {
public enum State: String, Codable {
case provisioning, provisioned
}
public let id, deviceType: String
public let state: State
public let thingUUID: Int?
public let discoveryTimeout, installationTimeout: Int
public let configurationPayload: [String: String]? // NOT [String: Any]?
enum CodingKeys: String, CodingKey {
case id = "id"
case deviceType = "device_type"
case state = "state"
case thingUUID = "thing_uuid"
case discoveryTimeout = "discovery_timeout"
case installationTimeout = "installation_timeout"
case configurationPayload = "configuration_payload"
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id) ?? ""
deviceType = try values.decodeIfPresent(String.self, forKey: .deviceType) ?? ""
let stateRaw = try values.decodeIfPresent(String.self, forKey: .state) ?? ""
state = Registration.State(rawValue: stateRaw) ?? .provisioning
thingUUID = try values.decodeIfPresent(Int.self, forKey: .thingUUID)
discoveryTimeout = try values.decodeIfPresent(Int.self, forKey: .discoveryTimeout) ?? 0
installationTimeout = try values.decodeIfPresent(Int.self, forKey: .installationTimeout) ?? 0
configurationPayload = try values.decodeIfPresent([String: String].self, forKey: .configurationPayload)
}
}
Tests
let json = Data("""
{
"id": "0000-0000-0000-0000-000",
"device_type": "device",
"state": "provisioning",
"thing_uuid": 999999999,
"discovery_timeout": 10,
"installation_timeout": 90,
"configuration_payload": {
"title": "Some Title",
"url": "https://www.someurl.com/",
"category": "test",
"views": "9999"
}
}
""".utf8
)
let decoded = try JSONDecoder().decode(Registration.self, from: json)
print(decoded)
let encoded = try JSONEncoder().encode(decoded)
print(String(data: encoded, encoding: .utf8))
This is not possible with the Codable protocol, because you do not know the type before hand. You'll have to either write your own method or have a different decoding strategy.
let json = """
{
"id": "0000-0000-0000-0000-000",
"device_type": "device",
"state": "provisioning",
"thing_uuid": 999999999,
"discovery_timeout": 10,
"installation_timeout": 90,
"configuration_payload": {
"title": "Some Title",
"url": "https://www.someurl.com/",
"category": "test",
"views": 9999
}
}
""".data(using: .utf8)
do {
let decoded = try? Registration.init(jsonData: json!)
print(decoded)
}catch {
print(error)
}
public struct Registration {
public enum State: String, Codable {
case provisioning, provisioned
}
public let id: String
public let device_type: String
public let state: State
public let error: String?
public let thing_uuid: Int?
public let discovery_timeout, installation_timeout: Int
public let configuration_payload: [String: Any]?
public init(jsonData: Data) throws {
let package = try JSONSerialization.jsonObject(with: jsonData, options: []) as! [String : Any]
id = package["id"] as! String
device_type = package["device_type"] as! String
state = State(rawValue: package["state"] as! String)!
error = package["error"] as? String
thing_uuid = package["thing_uuid"] as? Int
discovery_timeout = package["discovery_timeout"] as! Int
installation_timeout = package["installation_timeout"] as! Int
configuration_payload = package["configuration_payload"] as? [String: Any]
}
}
This is one possible way to handle the different types. You could also create a struct containing keys and loop through them, I think this illustrates the basic idea though.
Edit:
if let remaining = package["configuration_payload"] as? Data,
let data = try? JSONSerialization.data(withJSONObject: remaining, options: []) as Data,
let string = String(data: data, encoding: .utf8) {
// store your string if you want it in string formatt
print(string)
}
If you have a list of possible keys, using optionals is another way you could employ Codable. You can mix keys this way - only the ones that are available will attempt to be encoded/decoded
import UIKit
public struct Registration: Codable {
public enum State: String, Codable {
case provisioning, provisioned
}
public let id, deviceType: String
public let state: State
public let error: String?
public let thingUuid: Int?
public let discoveryTimeout, installationTimeout: Int
public var configurationPayload: ConfigurationPayload?
}
// nested json can be represented as a codable struct
public struct ConfigurationPayload: Codable {
let title: String?
let url: String?
let category: String?
let views: Int?
let nonTitle: String?
let anotherUrl: String?
let someCategory: String?
let someViews: Int?
// computed properties aren't part of the coding strategy
// TODO: avoid duplication in loop
var jsonString: String {
let mirror = Mirror(reflecting: self).children
let parameters = mirror.compactMap({$0.label})
let values = mirror.map({$0.value})
let keyValueDict = zip(parameters, values)
var returnString: String = "{\n"
for (key, value) in keyValueDict {
if let value = value as? Int {
returnString.append("\"\(key)\": \"\(value)\n")
} else if let value = value as? String {
returnString.append("\"\(key)\": \"\(value)\n")
}
}
returnString.append("}")
return returnString
}
}
// your json has a preceding key of "registration", this is the type you will decode
public struct RegistrationParent: Codable {
var registration: Registration
}
let jsonDataA =
"""
{
"registration": {
"id": "0000-0000-0000-0000-000",
"device_type": "device",
"state": "provisioning",
"thing_uuid": 999999999,
"discovery_timeout": 10,
"installation_timeout": 90,
"configuration_payload":
{
"title": "Some Title",
"url": "https://www.someurl.com/",
"category": "test",
"views": 9999
}
}
}
""".data(using: .utf8)!
let jsonDataB =
"""
{
"registration": {
"id": "0000-0000-0000-0000-000",
"device_type": "device",
"state": "provisioning",
"thing_uuid": 999999999,
"discovery_timeout": 10,
"installation_timeout": 90,
"configuration_payload":
{
"non_title": "Some Title",
"another_url": "https://www.someurl.com/",
"some_category": "test",
"some_views": 9999
}
}
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
do {
var registrationA = try decoder.decode(RegistrationParent.self, from: jsonDataA)
print(registrationA.registration.configurationPayload?.jsonString ?? "{}")
var registrationB = try decoder.decode(RegistrationParent.self, from: jsonDataB)
print(registrationB.registration.configurationPayload?.jsonString ?? "{}")
} catch {
print(error)
}
here is configurationPayload is dictionary so your Registration struct look like below
struct Registration : Codable {
let configurationPayload : ConfigurationPayload?
let deviceType : String?
let discoveryTimeout : Int?
let id : String?
let installationTimeout : Int?
let state : String?
let thingUuid : Int?
enum CodingKeys: String, CodingKey {
case configurationPayload = "configuration_payload"
case deviceType = "device_type"
case discoveryTimeout = "discovery_timeout"
case id = "id"
case installationTimeout = "installation_timeout"
case state = "state"
case thingUuid = "thing_uuid"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
configurationPayload = ConfigurationPayload(from: decoder)
deviceType = try values.decodeIfPresent(String.self, forKey: .deviceType)
discoveryTimeout = try values.decodeIfPresent(Int.self, forKey: .discoveryTimeout)
id = try values.decodeIfPresent(String.self, forKey: .id)
installationTimeout = try values.decodeIfPresent(Int.self, forKey: .installationTimeout)
state = try values.decodeIfPresent(String.self, forKey: .state)
thingUuid = try values.decodeIfPresent(Int.self, forKey: .thingUuid)
}
}
and your ConfigurationPayload look like this
struct ConfigurationPayload : Codable {
let category : String?
let title : String?
let url : String?
let views : Int?
enum CodingKeys: String, CodingKey {
case category = "category"
case title = "title"
case url = "url"
case views = "views"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
category = try values.decodeIfPresent(String.self, forKey: .category)
title = try values.decodeIfPresent(String.self, forKey: .title)
url = try values.decodeIfPresent(String.self, forKey: .url)
views = try values.decodeIfPresent(Int.self, forKey: .views)
}
}

How can I use decodable in unspecified return data from API

when trying to parse return data from API im getting "The data couldn’t be read because it isn’t in the correct format." because the return is inconsistent.
When logo_url has value it was a object see example below:
"logo_url": {
"mime_type": "image/jpeg",
"url": "http://google.com"
},
But when it doenst have value its return empty array
"logo_url": [],
This is the reason why im getting "The data couldn’t be read because it isn’t in the correct format."
My model
struct Model: Decodable {
let logo: Logo?
enum CodingKeys: String, CodingKey {
case logo = "logo_url"
}
}
struct Logo: Decodable {
let mimeType: String?
let url: String?
enum CodingKeys: String, CodingKey {
case mimeType = "mime_type"
case url
}
}
If you can't change this badly written API, you'd need a custom decoder, where you basically attempt to decode as the type you want, and failing that - make it nil:
struct Model: Decodable {
let logo: Logo?
enum CodingKeys: String, CodingKey {
case logo = "logo_url"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let logo = try? container.decode(Logo.self, forKey: .logo) {
self.logo = logo
} else {
self.logo = nil
}
}
}
I personally prefer checking if the logo_url is an array first, then let Swift report error if there is any happens by using try instead of try? when trying to decode a key. Since in most cases, you may want to know why your decoding failed instead of just getting nil as a result.
Additionally, you may want to use .convertFromSnakeCase as a keyDecodingStrategy so you don't have to write extra code.
let json2 = """
{
"logo_url": {
"mime_type": "image/jpeg",
"url": "http://google.com"
}
}
"""
let json3 = "[]"
struct Logo: Decodable {
let mimeType: String
let url: String
}
struct Model: Decodable {
let logo: Logo?
private enum CodingKeys: String, CodingKey {
case logoUrl
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if (try? container.decode([String].self, forKey: .logoUrl)) != nil {
self.logo = nil
} else {
self.logo = try container.decode(Logo.self, forKey: .logoUrl)
}
}
}
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result2 = try decoder.decode(Model.self, from: json2.data(using: .utf8)!)
print(result2)
let result3 = try? decoder.decode(Model.self, from: json3.data(using: .utf8)!)
print(result3)

Decoding json data in swift with two different possible json reponse from server to two different structs

When I make an api request through urlSession to the server I might get a json like
{
"movie": "Avengers",
"director": "Joss Whedon",
}
or like this
{
"apiKey": "invalid"
}
I have two structs to save like this
struct Movie: Codable {
let request: String
let director: String
init(movie:String, director:Sring) {
self.movie = movie
self.director = director
}
}
struct Valid: Codable {
let apiKey: String
init(apiKey:String) {
self.apiKey = apiKey
}
}
Based on the response i want to either decode to first struct or the second struct. How to do that.
if let url = URL(string: "myurl.com") {
URLSession.shared.dataTask(with: url) { data, response, error in
if let data = data {
guard let json = data as? [String:AnyObject] else {
return
}
do {
if let movie = json["movie"]{
//moview is there. so you can decode to your movie struct
let res = try JSONDecoder().decode(Movie.self, from: data)
}else{
//move is not there.it means error
let res = try JSONDecoder().decode(Valid.self, from: data)
}
} catch let error {
print(error)
}
}
}.resume()
}
Parse json with a single Struct like this
struct RootClass : Codable {
let apiKey : String?
let director : String?
let movie : String?
enum CodingKeys: String, CodingKey {
case apiKey = "apiKey"
case director = "director"
case movie = "movie"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
apiKey = try values.decodeIfPresent(String.self, forKey: .apiKey)
director = try values.decodeIfPresent(String.self, forKey: .director)
movie = try values.decodeIfPresent(String.self, forKey: .movie)
}
}
And check the value of key apiKey if it's nil then use movies and director. or use the value of apiKey
First of all, the Codable types to parse the above 2 JSON responses should look like,
struct Movie: Decodable {
let movie: String
let director: String
}
struct Valid: Decodable {
let apiKey: String
}
There is no need to explicitly create init(). Codable will handle that automatically.
Next, you need to create another Codable type that can handle both the responses, i.e.
enum Response: Decodable {
case movie(Movie)
case valid(Valid)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let data = try container.decode(Movie.self)
self = .movie(data)
} catch {
let data = try container.decode(Valid.self)
self = .valid(data)
}
}
}
Now, you can parse the JSON data like so,
do {
let response = try JSONDecoder().decode(Response.self, from: data)
print(response)
} catch {
print(error)
}

Serialize JSON string that contains escaped (backslash and double quote) Swift return Badly formed object

I have response string from the backend like this:
{
"status": "success",
"data": "{\"name\":\"asd\",\"address\":\"Street 1st\"}"
}
I think the problem was because the double quote (") in the data String. When I remove the double quote, the serialization was success. But the response is from backend and I have to deal with it.
Anyone can solve this problem?
Thank you.
Here is the playground code.
import Foundation
var jsonStr = """
{
"status": "success",
"data": "{\"name\":\"asd\",\"address\":\"Street 1st\"}"
}
"""
let data = jsonStr.data(using: .utf8)
if let d = data {
do {
let o = try JSONSerialization.jsonObject(with: d)
print(o)
} catch let e {
print(e)
}
} else {
print("DATA conversion ERROR")
}
First of all if you wrap the JSON in the literal string syntax of Swift 4 you have to escape the backslashes.
let jsonStr = """
{
"status": "success",
"data": "{\\"name\\":\\"asd\\",\\"address\\":\\"Street 1st\\"}"
}
"""
You got nested JSON. The value for key data is another JSON string which must be deserialized separately
let jsonData = Data(jsonStr.utf8)
do {
if let object = try JSONSerialization.jsonObject(with: jsonData) as? [String:String] {
print(object)
if let dataString = object["data"] as? String {
let dataStringData = Data(dataString.utf8)
let dataObject = try JSONSerialization.jsonObject(with: dataStringData) as? [String:String]
print(dataObject)
}
}
} catch {
print(error)
}
Or – with a bit more effort but – much more comfortable with the (De)Codable protocol
struct Response : Decodable {
private enum CodingKeys : String, CodingKey { case status, data }
let status : String
let person : Person
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decode(String.self, forKey: .status)
let dataString = try container.decode(String.self, forKey: .data)
person = try JSONDecoder().decode(Person.self, from: Data(dataString.utf8))
}
}
struct Person : Decodable {
let name, address : String
}
let jsonStr = """
{
"status": "success",
"data": "{\\"name\\":\\"asd\\",\\"address\\":\\"Street 1st\\"}"
}
"""
let jsonData = Data(jsonStr.utf8)
do {
let result = try JSONDecoder().decode(Response.self, from: jsonData)
print(result)
} catch {
print(error)
}
You have an error in your code ,as you write it in code like that """json""",
also
data is String not dictionary , so you will need to convert to data then JSONSerialization again
check code i add your response in Json file and parse it , and work correctly
So just write this in json file called it data.json
{
"status": "success",
"data": "{\"name\":\"asd\",\"address\":\"Street 1st\"}"
}
and use this :
guard let jsonFile = Bundle.main.path(forResource: "data", ofType: "json") else { return}
guard let data = try? Data(contentsOf: URL(fileURLWithPath: jsonFile), options: .mappedIfSafe) else {return}
if let response = try? JSONSerialization.jsonObject(with: data, options: .mutableLeaves) {
print(response)
if let dataInDictionary = response as? [String:Any] , let addresData = dataInDictionary["data"] as? String {
if let jsonData = addresData.data(using: .utf8),
let dictionary = try? JSONSerialization.jsonObject(with: jsonData, options: .mutableLeaves) as? [String:Any]{
print(dictionary)
}
}
}
Here is another example based on the answer #vadian
Swift 4 - Using Codable
This was the json that I received:
{
"error_code": 0,
"result": {
"responseData": "{\"emeter\":{\"get_realtime\":{\"voltage_mv\":237846,\"current_ma\":81,\"power_mw\":7428,\"total_wh\":1920,\"err_code\":0}}}"
}
}
The JSON part with backslashes is equal to this:
{
"emeter": {
"get_realtime": {
"voltage_mv": 237846,
"current_ma": 81,
"power_mw": 7428,
"total_wh":19201,
"err_code":0
}
}
}
And this was the code that I used:
import Foundation
class RealtimeEnergy: Codable {
let errorCode: Int
let result: ResultRealtimeEnergy?
let msg: String?
enum CodingKeys: String, CodingKey {
case errorCode = "error_code"
case result, msg
}
init(errorCode: Int, result: ResultRealtimeEnergy?, msg: String?) {
self.errorCode = errorCode
self.result = result
self.msg = msg
}
}
class ResultRealtimeEnergy: Codable {
let responseData: String
var emeter: Emeter
enum CodingKeys: String, CodingKey {
case responseData
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
responseData = try container.decode(String.self, forKey: .responseData)
let dataString = try container.decode(String.self, forKey: .responseData)
emeter = try JSONDecoder().decode(Emeter.self, from: Data(dataString.utf8))
}
}
class Emeter: Codable {
let emeter: EmeterClass
init(emeter: EmeterClass) {
self.emeter = emeter
}
}
class EmeterClass: Codable {
let getRealtime: GetRealtime
enum CodingKeys: String, CodingKey {
case getRealtime = "get_realtime"
}
init(getRealtime: GetRealtime) {
self.getRealtime = getRealtime
}
}
class GetRealtime: Codable {
let voltageMv, currentMa, powerMw, totalWh: Int
let errCode: Int
enum CodingKeys: String, CodingKey {
case voltageMv = "voltage_mv"
case currentMa = "current_ma"
case powerMw = "power_mw"
case totalWh = "total_wh"
case errCode = "err_code"
}
init(voltageMv: Int, currentMa: Int, powerMw: Int, totalWh: Int, errCode: Int) {
self.voltageMv = voltageMv
self.currentMa = currentMa
self.powerMw = powerMw
self.totalWh = totalWh
self.errCode = errCode
}
}

Resources