I am having issues with decoding JSON, where some of the fields returned from the server as not available on the client. Take a look at the following code. The JSON consists of three roles but the Role enum only consists of student and staff. How can I successfully decode JSON ignoring the missing faculty role.
let json = """
[
{
"role": "student"
},
{
"role": "staff"
},
{
"role": "faculty"
},
]
"""
struct User: Decodable {
let role: Role
}
enum Role: String, Decodable {
case student
case staff
private enum CodingKeys: String, CodingKey {
case student = "student"
case staff = "staff"
}
}
let decoded = try! JSONDecoder().decode([User].self, from: json.data(using: .utf8)!)
print(decoded)
Currently, I get the following error:
error: 'try!' expression unexpectedly raised an error: Swift.DecodingError.dataCorrupted(Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 2", intValue: 2), CodingKeys(stringValue: "role", intValue: nil)], debugDescription: "Cannot initialize Role from invalid String value faculty", underlyingError: nil))
It will always throw that error because the decoder will be presented with "role": "faculty" which it won't recognize. Thus if you really really want to ignore it you can ditch the decoder and do something like this:
guard let json = try? JSONSerialization.jsonObject(with: json.data(using: .utf8)!, options: .mutableContainers) as? [[String: Any]] else {return}
let users: [User] = json.filter({$0["role"] != "faculty")}.map({User(role: Role(rawValue: $0["role"])!)})
Or you can add the missing case and filter the array:
users = users.filter({$0 != Role.faculty})
You'll need to create a top-level object to hold the [User]:
struct UserList {
var users: [User]
}
Then, give it a custom decoder that will check for malformed role keys and ignore those Users, but won't ignore other errors:
extension UserList: Decodable {
// An empty object that can decode any keyed container by ignoring all keys
private struct Ignore: Decodable {}
init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var users: [User] = []
while !container.isAtEnd {
do {
// Try to decode a User
users.append(try container.decode(User.self))
}
// Check for the specific failure of "role" not decoding.
// Other errors will still be thrown normally
catch DecodingError.dataCorrupted(let context)
where context.codingPath.last?.stringValue == "role" {
// You still need to decode the object into something, but you can ignore it.
_ = try container.decode(Ignore.self)
}
}
self.users = users
}
}
let decoded = try JSONDecoder().decode(UserList.self,
from: json.data(using: .utf8)!).users
// [User(role: Role.student), User(role: Role.staff)]
If you define User.CodingKeys, then you can skip the Ignore struct:
struct User: Decodable {
// The auto-generated CodingKeys is private, so you have to
// define it by hand to access it elsewhere
enum CodingKeys: String, CodingKey {
case role
}
let role: Role
}
With that, you can get rid of the Ignore struct and replace this line:
_ = try container.decode(Ignore.self)
with:
_ = try container.nestedContainer(keyedBy: User.CodingKeys.self)
Related
I have this json:
{ "stuff": [
{
"type":"car",
"object":{
"a":66,
"b":66,
"c":66 }},
{
"type":"house",
"object":{
"d":66,
"e":66,
"f":66 }},
{
"type":"car",
"object":{
"a":66,
"b":66,
"c":66 }}
]}
As you can see for "car" and "house" there are different "object" structs, but both under the tag "object".
It would be ideal if one ended up with something like
struct StuffItem: Decodable {
let type: TheType
let car: Car
let house: House
}
Is there some Codable, swifty, way to handle this?
The swiftiest way in my opinion is an enum with associated types
This is valid JSON
let jsonString = """
{ "stuff": [
{
"type":"car",
"object":{
"a":66,
"b":66,
"c":66
}
},{
"type":"house",
"object":{
"d":66,
"e":66,
"f":66
}
},{
"type":"car",
"object":{
"a":66,
"b":66,
"c":66
}
}
]}
"""
These are the structs
struct Root : Decodable {
let stuff : [Object]
}
enum Type : String, Decodable { case car, house }
struct Car : Decodable {
let a, b, c : Int
}
struct House : Decodable {
let d, e, f : Int
}
enum Object : Decodable {
case house(House), car(Car)
private enum CodingKeys : String, CodingKey { case type, object }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let type = try container.decode(Type.self, forKey: .type)
switch type {
case .car:
let carData = try container.decode(Car.self, forKey: .object)
self = .car(carData)
case .house:
let houseData = try container.decode(House.self, forKey: .object)
self = .house(houseData)
}
}
}
And the code to decode the JSON
do {
let result = try JSONDecoder().decode(Root.self, from: Data(jsonString.utf8))
let objects = result.stuff
for object in objects {
switch object {
case .car(let car): print(car)
case .house(let house): print(house)
}
}
} catch {
print(error)
}
You can handle multiple cases by using the enum just define your type, Giving code will help you to parse the JSON by using Struct modal with enum.
// MARK: - Welcome
struct Welcome: Codable {
let stuff: [Stuff]
}
// MARK: - Stuff
struct Stuff: Codable {
let type: String
let object: Object
}
// MARK: - Object
struct Object: Codable {
let a, b, c, d: Int?
let e, f: Int?
}
enum Type: String {
case car
case house
}
func fetchResponse() {
do {
let jsonString = "your json string"
let data = Data(jsonString.utf8)
let result = try JSONDecoder().decode(Welcome.self, from: data)
let objects = result.stuff
let carObjects = objects.filter{$0.type == Type.car.rawValue}
print("Its car array: \(carObjects)")// if you need filters car object then use this
let houseObjects = objects.filter{$0.type == Type.house.rawValue}// if you need filters house object then use this
print("Its house array: \(houseObjects)")
// or you check in loop also
objects.forEach { (stuff) in
switch stuff.type {
case Type.car.rawValue:
print("Its car object")
case Type.house.rawValue:
print("Its house object")
default:
print("Also you can set your one case in `default`")
break
}
}
} catch {
print(error.localizedDescription)
}
}
Another explantion:
Since there are not many explanations of this around, here's another example of what #vadian has explained:
1. In Swift, use enum instead of struct to achieve 'flexible type'.
2. You then need parsers for the 'item', and for the 'type'.
2. Parse, and then just switch to decode, and set the correct type.
So for the JSON above, you'd have
struct YourFeed: Decodable {
let stuff: [Item]
}
Each item can be a Car or a House.
struct Car: Decodable { ... }
struct House: Decodable { ... }
So those are easy.
Now for Item. It can be more than one type.
In Swift, use enum instead of struct to achieve 'flexible type'.
// in Swift, an "enum" is basically a "struct" which can have a flexible type,
// so here we have enum Item rather than struct Item:
enum Item: Decodable {
// so this thing, Item, can be one of these two types:
case car(Car)
case house(House)
Next, simply mirror that in a raw enum which will be used for parsing the "type" field. (You can call it anything, I've just called it "Parse".)
// the relevant key strings for parsing the "type" field:
private enum Parse: String, Decodable {
case car
case house
}
Next, look at the original JSON up top. Each "item" has two fields, "type" and "object". Here they are in a raw enum. (Again you can call it anything, I've just called it "Keys" here.)
// we're decoding an item, what are the top-level tags in item?
private enum Keys: String, CodingKey {
// so, these are just the two fields in item from the json
case type
case object
}
Have an enum to parse the 'item' level, and an enum to parse the 'type'.
Finally, write the initializer for "Item". Simply decode the both the top level and the "type" ...
init(from decoder: Decoder) throws {
// parse the top level
let c = try decoder.container(keyedBy: Keys.self)
// and parse the 'type' field
let t = try c.decode(Parse.self, forKey: .type)
... and you're done. Decode the data (using the relevant class), and set the "Item" enum object to the appropriate type.
Parse those, and then just switch to decode / set the enum.
// we're done, so depending on which of the types it is,
// decode (using the relevant decoder), and become the relevant type:
switch t {
case .car:
let d = try c.decode(Car.self, forKey: .object)
self = .car(d)
case .house:
let d = try c.decode(House.self, forKey: .object)
self = .house(d)
}
}
}
Here's the whole thing in one go:
enum Item: Decodable {
case car(Car)
case house(House)
// the relevant key strings for parsing the 'type' field:
private enum Parse: String, Decodable {
case car
case house
}
// the top-level tags in 'item':
private enum Keys: String, CodingKey {
case type
case object
}
init(from decoder: Decoder) throws {
// parse the top level
let c = try decoder.container(keyedBy: Keys.self)
// parse the 'type' field
let t = try c.decode(Parse.self, forKey: .type)
// we're done, switch to
// decode (using the relevant decoder), and become the relevant type:
switch t {
case .car:
let d = try c.decode(Car.self, forKey: .object)
self = .car(d)
case .house:
let d = try c.decode(House.self, forKey: .object)
self = .house(d)
}
}
}
Lets say i have this json from an API request:
friends: {
"john":31,
"mark":27,
"lisa":17,
"tom":41
}
I usually expect it in an array format:
friends: [
{ "john":31 },
{ "mark":27 },
{ "lisa":17 },
{ "tom":41 }
]
But the API doesn't provide me this way the results. So i want finally to map it to an array of [Friend], where Friend is:
class Friend: Decodable {
let name: String
let age: Int
}
How should i serialize this json to get [Friend] ?
First of all, example isn't valid json at all. To be valid it either shouldn't include "friends" label, or it should be embedded in another object like this
{
"friends": {
"john":31,
"mark":27,
"lisa":17,
"tom":41
}
}
If I understand question correctly, you want to decode json object to swift array. I don't think there is a way to do so without writing custom decoding. Instead, you can decode json into Dictionary and when manually map it like so
struct Friend {
let name: String
let age: Int
}
struct Friends: Decodable {
let friends: [String: Int]
}
let friends = try! JSONDecoder().decode(Friends.self, from: json.data(using: .utf8)!)
.friends
.map { (name, age) in Friend(name: name, age: age) }
Disclaimer: I recommend to change you API format to one like in #scriptable's answer (which was deleted while I was answering, hm), where name and age fields are properly defined. And where you're not limited to basically a pair of key-value to parse.
But if you can't or won't change it you may use something like this to decode your Friend type:
struct Friend: Decodable {
let name: String
let age: UInt
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dictionary = try container.decode([String: UInt].self)
guard dictionary.count == 1, let (name, age) = dictionary.first else {
throw DecodingError.invalidFriendDictionary
}
self.name = name
self.age = age
}
enum DecodingError: Error {
case invalidFriendDictionary
}
}
struct Friends: Decodable {
let friends: [Friend]
}
let friends = try JSONDecoder().decode(Friends.self, from: data)
It assumes key is name and value is age. And checks that there's only a one pair to parse.
I have the following code to extract a JSON contained within a coding key:
let value = try! decoder.decode([String:Applmusic].self, from: $0["applmusic"])
This successfully handles the following JSONs:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
}
However, fails to extract a JSON with the coding key of applmusic from the following one:
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry",
},
"spotify":{
"differentcode":"SPOT",
"music_quality":"good",
"spotify_specific_code":"absent in apple"
},
"amazon":{
"amzncode":"SPOT",
"music_quality":"good",
"stanley":"absent in apple"
}
}
The data models for applmusic,spotify and amazon are different. However, I need only to extract applmusic and omit other coding keys.
My Swift data model is the following:
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
The API responds with the full JSON and I cannot ask it to give me only the needed fields.
How to decode only the specific part of the json? It seems, that Decodable requires me to deserialize the whole json first, so I have to know the full data model for it.
Obviously, one of the solutions would be to create a separate Response model just to contain the applmusicparameter, but it looks like a hack:
public struct Response: Codable {
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
// The only parameter is `applmusic`, ignoring the other parts - works fine
public let applmusic: Applmusic
}
Could you propose a better way to deal with such JSON structures?
A little bit more insight
I use it the following technique in the generic extension that automatically decodes the API responses for me. Therefore, I'd prefer to generalize a way for handling such cases, without the need to create a Root structure. What if the key I need is 3 layers deep in the JSON structure?
Here is the extension that does the decoding for me:
extension Endpoint where Response: Swift.Decodable {
convenience init(method: Method = .get,
path: Path,
codingKey: String? = nil,
parameters: Parameters? = nil) {
self.init(method: method, path: path, parameters: parameters, codingKey: codingKey) {
if let key = codingKey {
guard let value = try decoder.decode([String:Response].self, from: $0)[key] else {
throw RestClientError.valueNotFound(codingKey: key)
}
return value
}
return try decoder.decode(Response.self, from: $0)
}
}
}
The API is defined like this:
extension API {
static func getMusic() -> Endpoint<[Applmusic]> {
return Endpoint(method: .get,
path: "/api/music",
codingKey: "applmusic")
}
}
Updated: I made an extension of JSONDecoder out of this answer, you can check it here: https://github.com/aunnnn/NestedDecodable, it allows you to decode a nested model of any depth with a key path.
You can use it like this:
let post = try decoder.decode(Post.self, from: data, keyPath: "nested.post")
You can make a Decodable wrapper (e.g., ModelResponse here), and put all the logic to extract nested model with a key inside that:
struct DecodingHelper {
/// Dynamic key
private struct Key: CodingKey {
let stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = nil
}
let intValue: Int?
init?(intValue: Int) {
return nil
}
}
/// Dummy model that handles model extracting logic from a key
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
let key = Key(stringValue: decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String)!
let values = try decoder.container(keyedBy: Key.self)
nested = try values.decode(NestedModel.self, forKey: key)
}
}
static func decode<T: Decodable>(modelType: T.Type, fromKey key: String) throws -> T {
// mock data, replace with network response
let path = Bundle.main.path(forResource: "test", ofType: "json")!
let data = try Data(contentsOf: URL(fileURLWithPath: path), options: .mappedIfSafe)
let decoder = JSONDecoder()
// ***Pass in our key through `userInfo`
decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!] = key
let model = try decoder.decode(ModelResponse<T>.self, from: data).nested
return model
}
}
You can pass your desired key through userInfo of JSONDecoder ("my_model_key"). It is then converted to our dynamic Key inside ModelResponse to actually extract the model.
Then you can use it like this:
let appl = try DecodingHelper.decode(modelType: Applmusic.self, fromKey: "applmusic")
let amazon = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "amazon")
let spotify = try DecodingHelper.decode(modelType: Spotify.self, fromKey: "spotify")
print(appl, amazon, spotify)
Full code:
https://gist.github.com/aunnnn/2d6bb20b9dfab41189a2411247d04904
Bonus: Deeply nested key
After playing around more, I found you can easily decode a key of arbitrary depth with this modified ModelResponse:
private struct ModelResponse<NestedModel: Decodable>: Decodable {
let nested: NestedModel
public init(from decoder: Decoder) throws {
// Split nested paths with '.'
var keyPaths = (decoder.userInfo[CodingUserInfoKey(rawValue: "my_model_key")!]! as! String).split(separator: ".")
// Get last key to extract in the end
let lastKey = String(keyPaths.popLast()!)
// Loop getting container until reach final one
var targetContainer = try decoder.container(keyedBy: Key.self)
for k in keyPaths {
let key = Key(stringValue: String(k))!
targetContainer = try targetContainer.nestedContainer(keyedBy: Key.self, forKey: key)
}
nested = try targetContainer.decode(NestedModel.self, forKey: Key(stringValue: lastKey)!)
}
Then you can use it like this:
let deeplyNestedModel = try DecodingHelper.decode(modelType: Amazon.self, fromKey: "nest1.nest2.nest3")
From this json:
{
"apple": { ... },
"amazon": {
"amzncode": "SPOT",
"music_quality": "good",
"stanley": "absent in apple"
},
"nest1": {
"nest2": {
"amzncode": "Nest works",
"music_quality": "Great",
"stanley": "Oh yes",
"nest3": {
"amzncode": "Nest works, again!!!",
"music_quality": "Great",
"stanley": "Oh yes"
}
}
}
}
Full code: https://gist.github.com/aunnnn/9a6b4608ae49fe1594dbcabd9e607834
You don't really need the nested struct Applmusic inside Response. This will do the job:
import Foundation
let json = """
{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
"""
public struct Applmusic: Codable {
public let code: String
public let quality: String
public let line: String
}
public struct Response: Codable {
public let applmusic: Applmusic
}
if let data = json.data(using: .utf8) {
let value = try! JSONDecoder().decode(Response.self, from: data).applmusic
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
Edit: Addressing your latest comment
If the JSON response would change in a way that the applmusic tag is nested, you would only need to properly change your Response type. Example:
New JSON (note that applmusic is now nested in a new responseData tag):
{
"responseData":{
"applmusic":{
"code":"AAPL",
"quality":"good",
"line":"She told me don't worry"
},
"I don't want this":"potatoe",
}
}
The only change needed would be in Response:
public struct Response: Decodable {
public let applmusic: Applmusic
enum CodingKeys: String, CodingKey {
case responseData
}
enum ApplmusicKey: String, CodingKey {
case applmusic
}
public init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let applmusicKey = try values.nestedContainer(keyedBy: ApplmusicKey.self, forKey: .responseData)
applmusic = try applmusicKey.decode(Applmusic.self, forKey: .applmusic)
}
}
The previous changes wouldn't break up any existing code, we are only fine-tuning the private implementation of how the Response parses the JSON data to correctly fetch an Applmusic object. All calls such as JSONDecoder().decode(Response.self, from: data).applmusic would remain the same.
Tip
Finally, if you want to hide the Response wrapper logic altogether, you may have one public/exposed method which will do all the work; such as:
// (fine-tune this method to your needs)
func decodeAppleMusic(data: Data) throws -> Applmusic {
return try JSONDecoder().decode(Response.self, from: data).applmusic
}
Hiding the fact that Response even exists (make it private/inaccessible), will allow you to have all the code through your app only have to call decodeAppleMusic(data:). For example:
if let data = json.data(using: .utf8) {
let value = try! decodeAppleMusic(data: data)
print(value) // Applmusic(code: "AAPL", quality: "good", line: "She told me don\'t worry")
}
Recommended read:
Encoding and Decoding Custom Types
https://developer.apple.com/documentation/foundation/archives_and_serialization/encoding_and_decoding_custom_types
Interesting question. I know that it was 2 weeks ago but I was wondering
how it can be solved using library KeyedCodable I created. Here is my proposition with generic:
struct Response<Type>: Codable, Keyedable where Type: Codable {
var responseObject: Type!
mutating func map(map: KeyMap) throws {
try responseObject <-> map[map.userInfo.keyPath]
}
init(from decoder: Decoder) throws {
try KeyedDecoder(with: decoder).decode(to: &self)
}
}
helper extension:
private let infoKey = CodingUserInfoKey(rawValue: "keyPath")!
extension Dictionary where Key == CodingUserInfoKey, Value == Any {
var keyPath: String {
set { self[infoKey] = newValue }
get {
guard let key = self[infoKey] as? String else { return "" }
return key
}
}
use:
let decoder = JSONDecoder()
decoder.userInfo.keyPath = "applmusic"
let response = try? decoder.decode(Response<Applmusic>.self, from: jsonData)
Please notice that keyPath may be nested more deeply I mean it may be eg. "responseData.services.applemusic".
In addition Response is a Codable so you can encode it without any additional work.
I'm using the Decodable protocol in order to parse JSON received from an external source. After decoding the attributes that I do know about there still may be some attributes in the JSON that are unknown and have not yet been decoded. For example, if the external source added a new attribute to the JSON at some future point in time I would like to hold onto these unknown attributes by storing them in a [String: Any] dictionary (or an alternative) so the values do not get ignored.
The issue is that after decoding the attributes that I do know about there isn't any accessors on the container to retrieve the attributes that have not yet been decoded. I'm aware of the decoder.unkeyedContainer() which I could use to iterate over each value however this would not work in my case because in order for that to work you need to know what value type you're iterating over but the value types in the JSON are not always identical.
Here is an example in playground for what I'm trying to achieve:
// Playground
import Foundation
let jsonData = """
{
"name": "Foo",
"age": 21
}
""".data(using: .utf8)!
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
// I would like to store the `age` attribute in this dictionary
// but it would not be known at the time this code was written.
self.unknownAttributes = [:]
}
}
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
// The `person.unknownAttributes` dictionary should
// contain the "age" attribute with a value of 21.
I would like for the unknownAttributes dictionary to store the age attribute and value in this case and any other possible value types if they get added to the JSON from the external source in the future.
The reason I am wanting to do something like this is so that I can persist the unknown attributes present in the JSON so that in a future update of the code I will be able to handle them appropriately once the attribute keys are known.
I've done plenty of searching on StackOverflow and Google but haven't yet encountered this unique case. Thanks in advance!
You guys keep coming up with novel ways to stress the Swift 4 coding APIs... ;)
A general solution, supporting all value types, might not be possible. But, for primitive types, you can try this:
Create a simple CodingKey type with string-based keys:
struct UnknownCodingKey: CodingKey {
init?(stringValue: String) { self.stringValue = stringValue }
let stringValue: String
init?(intValue: Int) { return nil }
var intValue: Int? { return nil }
}
Then write a general decoding function using the standard KeyedDecodingContainer keyed by the UnknownCodingKey above:
func decodeUnknownKeys(from decoder: Decoder, with knownKeys: Set<String>) throws -> [String: Any] {
let container = try decoder.container(keyedBy: UnknownCodingKey.self)
var unknownKeyValues = [String: Any]()
for key in container.allKeys {
guard !knownKeys.contains(key.stringValue) else { continue }
func decodeUnknownValue<T: Decodable>(_ type: T.Type) -> Bool {
guard let value = try? container.decode(type, forKey: key) else {
return false
}
unknownKeyValues[key.stringValue] = value
return true
}
if decodeUnknownValue(String.self) { continue }
if decodeUnknownValue(Int.self) { continue }
if decodeUnknownValue(Double.self) { continue }
// ...
}
return unknownKeyValues
}
Finally, use the decodeUnknownKeys function to fill your unknownAttributes dictionary:
struct Person: Decodable {
enum CodingKeys: CodingKey {
case name
}
let name: String
let unknownAttributes: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let knownKeys = Set(container.allKeys.map { $0.stringValue })
self.unknownAttributes = try decodeUnknownKeys(from: decoder, with: knownKeys)
}
}
A simple test:
let jsonData = """
{
"name": "Foo",
"age": 21,
"token": "ABC",
"rate": 1.234
}
""".data(using: .utf8)!
let decoder = JSONDecoder()
let person = try! decoder.decode(Person.self, from: jsonData)
print(person.name)
print(person.unknownAttributes)
prints:
Foo
["age": 21, "token": "ABC", "rate": 1.234]
I have the following response from a JSON query. How do I represent a dictionary as codable? I have shortened the JSON response to save space.
{
"result":[
{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": {
"link": "https://foo.com/api/now/table/cmn_location/753ac76f4fd9320066bfb63ca310c79b",
"value": "753ac76f4fd9320066bfb63ca310c79b"
}
}
]
}
struct ResultList : Codable {
let result: [Result]
}
struct Result : Codable {
let delivery_address: String
let made_sla: String
let watch_list: String
let upon_reject: String
let location: Location
}
struct Location: Codable {
let link: String?
let value: String?
}
let decoder = JSONDecoder()
do {
let todo = try decoder.decode(ResultList.self, from: responseData)
print("todo \(todo)")
} catch {
print("error trying to convert data to JSON")
print(error)
}
I'm getting the following error:
"Expected to decode Dictionary<String, Any> but found a string/data instead.", underlyingError: nil))
Based on all the comments, I believe the JSON you're actually decoding looks more like this:
{
"result": [{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": {
"link": "https://foo.com/api/now/table/cmn_location/753ac76f4fd9320066bfb63ca310c79b",
"value": "753ac76f4fd9320066bfb63ca310c79b"
}
},
{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": ""
}
]
}
So there are some records that have a location, and some that encode location as an empty string. Basically this is really messed up JSON on a whole lot of levels and whatever code generates it should be fixed. The bad news I'm sure that won't happen. The good news is we can fix it locally at least.
We're going to have to decode this by hand, so we might as well clean up all the rest of the mess while we're in here. The first thing is that the names don't match Swift naming conventions. We can fix that using CodingKeys. We can also provide real types (Bool and a Rejection enum) for the things that are currently mis-typed strings.
enum Rejection: String, Codable {
case cancel
}
struct Result : Codable {
let deliveryAddress: String
let madeSLA: Bool
let watchList: String
let uponReject: Rejection
let location: Location?
private enum CodingKeys: String, CodingKey {
case deliveryAddress = "delivery_address"
case madeSLA = "made_sla"
case watchList = "watch_list"
case uponReject = "upon_reject"
case location
}
}
Now we just need to be able to decode it. Notice that I made Location optional. Clearly it sometimes doesn't exist, so you either need a default value or it needs to be optional. I chose the latter. Decoding all of this is very straight forward:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
deliveryAddress = try container.decode(String.self, forKey: .deliveryAddress)
madeSLA = try container.decode(String.self, forKey: .madeSLA) == "true"
watchList = try container.decode(String.self, forKey: .watchList)
uponReject = try container.decode(Rejection.self, forKey: .uponReject)
location = try? container.decode(Location.self, forKey: .location)
}
The last line is your actual question. It just says if we can't decode it as a Location, set it to nil. We could be stricter here and try first to decode it as a Location, and then as a String, and then check if the String is empty, but it feels reasonable to use nil here for any decoding failure.
Assuming your JSON is (please note the missing closing brace)
{
"result": [
{
"delivery_address": "",
"made_sla": "true",
"watch_list": "",
"upon_reject": "cancel",
"location": {
"link": "https://blah/blah/foo",
"value": "fsfdfr32r34rwfwffas"
}
}
]
}
You can decode these structs
struct Root : Decodable {
let result : [Result]
struct Result : Decodable {
let location: Location
}
}
struct Location: Decodable {
let link: String
let value: String
}
with
JSONDecoder().decode(Root.self, from: data)