Decode json array as an object in swift

Viewed 1926

Im trying to decode this type of json in swift:

[{"Product":[{"Name":"exampleName"},{"Quantity":"1"}]}]

Therefore I have these structs:

struct DataModel: Codable {
    var product: Product?

    enum CodingKeys: String, CodingKey {
        case product = "Product"
    }
}

struct Product: Codable {
    var name, quantity: String?

    enum CodingKeys: String, CodingKey {
        case name = "Name"
        case quantity = "Quantity"
    }
}

and this function:

if let data = try? JSONDecoder().decode([DataModel].self, from: jsonData) {
     //continue with result...       
} else {
     print("failed")
}

Basically I want to achieve that my object contains all properties from the array.

Thanks in advance =)

2 Answers

First, I assume you'd really like the final result to be [Product] where Product looks like this:

struct Product {
    var name: String = ""
    var quantity: Int = 0
}

So if any keys are missing, they'll get the defaults. You could change this solution to throw an error in that case, but I think this approach is much nicer than Optionals.

Given that, here's how you decode it:

extension Product: Decodable {
    init(from decoder: Decoder) throws {
        // This is a very uniform type (all Strings). Swift can decode that
        // without much help.
        let container = try decoder
            .singleValueContainer()
            .decode([String: [[String: String]]].self)

        // Extract the Products. Throwing an error here might be nicer.
        let keyValues = container["Product"] ?? []

        // This loops over every Key-Value element, and then loops through
        // each one. We only expect one element in the second loop, though.
        for keyValue in keyValues {
            for (key, value) in keyValue {
                switch key {
                case "Name": self.name = value
                case "Quantity": self.quantity = Int(value) ?? 0
                default: break // Ignore unknown keys
                }
            }
        }
        // This solution just assigns defaults for missing keys, but
        // you could validate that everything was found, and throw an
        // error here if desired.
    }
}

let data = try JSONDecoder().decode([Product].self, from: jsonData)
// [{name "exampleName", quantity 1}]

data.first!.name
// "exampleName"

In most cases the above is probably fine, but it's also very sloppy about malformed data. It just returns a default object, which could make an error very hard to track down. This example goes in the other direction, and does all the error checking you would expect from a normal Decodable.

First, we'll need a CodingKey that can accept any String. I really don't understand why this isn't built into stdlib:

struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
    var stringValue: String
    init(stringValue: String) { self.stringValue = stringValue }
    init(_ stringValue: String) { self.init(stringValue: stringValue) }
    var intValue: Int?
    init?(intValue: Int) { return nil }
    init(stringLiteral value: String) { self.init(value) }
}

I also find DecodingErrors very cumbersome to build, so I often build little helper functions:

func keyNotFound(_ key: String, codingPath: [CodingKey]) -> Error {
    DecodingError.keyNotFound(AnyStringKey(key),
                              .init(codingPath: [],
                                    debugDescription: "\(key) key not found"))
}


func typeMismatch(_ key: String, expected: Any.Type, codingPath: [CodingKey]) -> Error {
    DecodingError.typeMismatch(expected, .init(codingPath: codingPath + [AnyStringKey(key)],
                                               debugDescription: "Expected \(expected)."))
}

With those in place, here's a more strict decoder:

extension Product: Decodable {
    init(from decoder: Decoder) throws {
        // This is a very uniform type (all Strings). Swift can decode that
        // without much help.
        let container = try decoder
            .singleValueContainer()
            .decode([String: [[String: String]]].self)

        var codingPath: [AnyStringKey] = []

        // Extract the Products. Throwing an error here might be nicer.
        guard let keyValues = container["Product"] else {
            throw keyNotFound("Product", codingPath: codingPath)
        }

        codingPath.append("Product")

        var name: String?
        var quantity: Int?

        // This loops over every Key-Value element, and then loops through
        // each one. We only expect one element in the second loop, though.
        for keyValue in keyValues {
            for (key, value) in keyValue {
                switch key {
                case "Name":
                    name = value

                case "Quantity":
                    guard let intValue = Int(value) else {
                        throw typeMismatch("Quantity",
                                           expected: Int.self,
                                           codingPath: codingPath)
                    }
                    quantity = intValue

                default: break // Ignore unknown keys
                }
            }
        }

        guard let name = name else {
            throw keyNotFound("Name", codingPath: codingPath)
        }
        self.name = name

        guard let quantity = quantity else {
            throw keyNotFound("Quantity", codingPath: codingPath)
        }
        self.quantity = quantity
    }
}

Something is wrong in the statement.

If a product is defined by a name and a quantity, then it is not an array.. So either the JSON is wrong, either the Data structure. Maybe both.

[{"Product":[{"Name":"exampleName"},{"Quantity":"1"}]}]

Your JSON should be:

[{"Name":"exampleName", "Quantity":"1"}]

And your model:

struct Product: Codable {
    var name, quantity: String

    // This can be omitted if 
    enum CodingKeys: String, CodingKey {
        case name = "Name"
        case quantity = "Quantity"
    }
}

Then:

do {
    var products = try JSONDecoder().decode([Product].self, from: jsonData)
} catch {
    print("Decoding Error : \(error)")
}

Should work perfectly, and fail if data are not complete.

See other answers for custom decoding that fills properties even if json data are missing.. I suppose it should not happen. If quantity is not provided, then you let your application continue with a meaningless number if you fill it by yourself.

By the way, I know it is another problem, but we usually not encode quantity in products in this kind of application.. We have the catalog on one side with products name and unique reference, and the stocks on another. It is more flexible to have two different requests: FetchCatalog, and FetchStocks.

Related