Swift Decodable from JSON using wildcard keys [duplicate] - json

I have a JSON object with incrementing names to parse and I want to store the output into an object with a name field and a list of pet field. I normally use JSONDecoder as its pretty handy and easy to use, but I don't want to hard-code the CodingKey as I think it is very bad practice.
Input:
{"shopName":"KindHeartVet", "pet1":"dog","pet2":"hamster","pet3":"cat", ...... "pet20":"dragon"}
The object that I want to store the result in is something like the following.
class VetShop: NSObject, Decodable {
var shopName: String?
var petList: [String]?
private enum VetKey: String, CodingKey {
case shopName
case petList
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: VetKey.self)
shopName = try? container.decode(String.self, forKey: .shopName)
// implement storing of petList here.
}
}
What I'm struggling a lot on is, as CodingKey is enum, its a let constants, so I can't modify (and shouldn't modify) a constant, but I need to map the petList to the "petN" field, where N is the incrementing number.
EDIT :
I definitely cannot change the API response structure because it is a public API, not something I developed, I'm just trying to parse and get the value from this API, hope this clear the confusion!

Codable has provisions for dynamic keys. If you absolutely can't change the structure of the JSON you're getting, you could implement a decoder for it like this:
struct VetShop: Decodable {
let shopName: String
let pets: [String]
struct VetKeys: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.stringValue = "\(intValue)";
self.intValue = intValue
}
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: VetKeys.self)
var pets = [String]()
var shopName = ""
for key in container.allKeys {
let str = try container.decode(String.self, forKey: key)
if key.stringValue.hasPrefix("pet") {
pets.append(str)
} else {
shopName = str
}
}
self.shopName = shopName
self.pets = pets
}
}

You can try this to parse your data as Dictionary. In this way, you can get all keys of the dictionary.
let url = URL(string: "YOUR_URL_HERE")
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else { return }
do {
let dics = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as! Dictionary<String, Any>
let keys = [String](dics.keys)
print(keys) // You have the key list
print(dics[keys[0]]) // this will print the first value
} catch let error as NSError {
print(error)
}
}).resume()
I hope you can figure out what you need to do.

Related

Swift - Codable struct with generic Dictionary var?

I have a Swift struct that looks like this:
struct MyStruct: Codable {
var id: String
var name: String
var createdDate: Date
}
To that, I would like to add another property: a [String:Any] dictionary. The result would look like this:
struct MyStruct: Codable {
var id: String
var name: String
var createdDate: Date
var attributes: [String:Any] = [:]
}
In the end, I would like to be able to serialize my MyStruct instance to a JSON string, and vice versa. However, when I go to build I get an error saying,
Type 'MyStruct' does not conform to protocol 'Codable'
Type 'MyStruct' does not conform to protocol 'Decodable'
It's clearly the attributes var that is tripping up my build, but I'm not sure how I can get the desired results. Any idea how I can code my struct to support this?
Since the comments already point out that Any type has nothing to do with generics, let me jump straight into the solution.
First thing you need is some kind of wrapper type for your Any attribute values. Enums with associated values are great for that job. Since you know best what types are to be expected as an attribute, feel free to add/remove any case from my sample implementation.
enum MyAttrubuteValue {
case string(String)
case date(Date)
case data(Data)
case bool(Bool)
case double(Double)
case int(Int)
case float(Float)
}
We will be later wrapping attribute values from the [String: Any] dictionary into the wrapper enum cases, but first we need to make the type conform to the Codable protocols. I am using singleValueContainer() for the decoding/encoding so the final json will produce a regular json dicts.
extension MyAttrubuteValue: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let date = try? container.decode(Date.self) {
self = .date(date)
} else if let data = try? container.decode(Data.self) {
self = .data(data)
} else if let bool = try? container.decode(Bool.self) {
self = .bool(bool)
} else if let double = try? container.decode(Double.self) {
self = .double(double)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else if let float = try? container.decode(Float.self) {
self = .float(float)
} else {
fatalError()
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let string):
try? container.encode(string)
case .date(let date):
try? container.encode(date)
case .data(let data):
try? container.encode(data)
case .bool(let bool):
try? container.encode(bool)
case .double(let double):
try? container.encode(double)
case .int(let int):
try? container.encode(int)
case .float(let float):
try? container.encode(float)
}
}
}
At this point we are good to go, but before we will decode/encode the attributes, we can use some extra interoperability between [String: Any] and [String: MyAttrubuteValue] types. To map easily between Any and MyAttrubuteValue lets add the following:
extension MyAttrubuteValue {
var value: Any {
switch self {
case .string(let value):
return value
case .date(let value):
return value
case .data(let value):
return value
case .bool(let value):
return value
case .double(let value):
return value
case .int(let value):
return value
case .float(let value):
return value
}
}
init?(_ value: Any) {
if let string = value as? String {
self = .string(string)
} else if let date = value as? Date {
self = .date(date)
} else if let data = value as? Data {
self = .data(data)
} else if let bool = value as? Bool {
self = .bool(bool)
} else if let double = value as? Double {
self = .double(double)
} else if let int = value as? Int {
self = .int(int)
} else if let float = value as? Float {
self = .float(float)
} else {
return nil
}
}
}
Now, with the quick value access and new init, we can map values easily. We are also making sure that the helper properties are only available for the dictionaries of concrete types, the ones we are working with.
extension Dictionary where Key == String, Value == Any {
var encodable: [Key: MyAttrubuteValue] {
compactMapValues(MyAttrubuteValue.init)
}
}
extension Dictionary where Key == String, Value == MyAttrubuteValue {
var any: [Key: Any] {
mapValues(\.value)
}
}
Now the final part, a custom Codable implementation for MyStruct
extension MyStruct: Codable {
enum CodingKeys: String, CodingKey {
case id = "id"
case name = "name"
case createdDate = "createdDate"
case attributes = "attributes"
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(name, forKey: .name)
try container.encode(createdDate, forKey: .createdDate)
try container.encode(attributes.encodable, forKey: .attributes)
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(String.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
createdDate = try container.decode(Date.self, forKey: .createdDate)
attributes = try container.decode(
[String: MyAttrubuteValue].self, forKey: .attributes
).any
}
}
This solution is fairly long, but pretty straight-forward at the same time. We lose automatic Codable implementation, but we got exactly what we wanted. Now you are able to encode ~Any~ type that already conforms to Codable easily, by adding an extra case to your new MyAttrubuteValue enum. One final thing to say is that we use similar approach to this one in production, and we have been happy so far.
That's a lot of code, here is a gist.

Parsing json blob field in Swift

I am reading JSON from a web service in swift which is in the following format
[{
"id":1,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"5555555",
"logo":[-1,-40,-1,-32],
"shopPath":"test"
},
{
"id":2,
"shopName":"test",
"shopBranch":"main",
"shopAddress":"usa",
"shopNumber":"66666666",
"logo":[-1,-50,-2,-2],
"shopPath":"test"
}]
I have managed to read all the strings easily but when it comes to the logo part I am not sure what should I do about it, this is a blob field in a mySQL database which represent an image that I want to retrieve in my swift UI, here is my code for doing that but i keep getting errors and not able to figure the right way to do it:
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: [String]
let path: String
}
func getBrandsJson() {
let url = URL(string: "http://10.211.55.4:8080/exam/Test")
URLSession.shared.dataTask(with: url!, completionHandler: {(data, response, error) in
guard let data = data, error == nil else {
print(error!);
return
}
print(response.debugDescription)
let decoder = JSONDecoder()
let classes = try! decoder.decode([Brand].self, from: data)
for myClasses in classes {
print(myClasses.branch)
if let imageData:Data = myClasses.logo.data(using:String.Encoding.utf8){
let image = UIImage(data:imageData,scale:1.0)
var imageView : UIImageView!
}
}
}).resume()
}
Can someone explain how to do that the right way I have searched a lot but no luck
First of all, you should better tell the server side engineers of the web service, that using array of numbers is not efficient to return a binary data in JSON and that they should use Base-64 or something like that.
If they are stubborn enough to ignore your suggestion, you should better decode it as Data in your custom decoding initializer.
struct Brand: Decodable {
private enum CodingKeys: String, CodingKey {
case id = "id"
case name = "shopName"
case branch = "shopBranch"
case address = "shopAddress"
case phone = "shopNumber"
case logo = "logo"
case path = "shopPath"
}
let id: Int
let name: String
let branch: String
let address: String
let phone: String
let logo: Data
let path: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int.self, forKey: CodingKeys.id)
self.name = try container.decode(String.self, forKey: CodingKeys.name)
self.branch = try container.decode(String.self, forKey: CodingKeys.branch)
self.address = try container.decode(String.self, forKey: CodingKeys.address)
self.phone = try container.decode(String.self, forKey: CodingKeys.phone)
self.path = try container.decode(String.self, forKey: CodingKeys.path)
//Decode the JSON array of numbers as `[Int8]`
let bytes = try container.decode([Int8].self, forKey: CodingKeys.logo)
//Convert the result into `Data`
self.logo = Data(bytes: bytes.lazy.map{UInt8(bitPattern: $0)})
}
}
And you can write the data decoding part of your getBrandsJson() as:
let decoder = JSONDecoder()
do {
//You should never use `try!` when working with data returned by server
//Generally, you should not ignore errors or invalid inputs silently
let brands = try decoder.decode([Brand].self, from: data)
for brand in brands {
print(brand)
//Use brand.logo, which is a `Data`
if let image = UIImage(data: brand.logo, scale: 1.0) {
print(image)
//...
} else {
print("invalid binary data as an image")
}
}
} catch {
print(error)
}
I wrote some lines to decode array of numbers as Data by guess. So if you find my code not working with your actual data, please tell me with enough description and some examples of actual data. (At least, you need to show me a first few hundreds of the elements in the actual "logo" array.)
Replace
let logo: [String]
with
let logo: [Int]
to get errors use
do {
let classes = try JSONDecoder().decode([Brand].self, from: data)
}
catch {
print(error)
}

Is it possible to decode additional parameters using JSONDecoder?

We have some response returned by backend:
{
"name": "Some name",
"number": 42,
............
"param0": value0,
"param1": value1,
"param2": value2
}
Model structure for response:
struct Model: Codable {
let name: String
let number: Int
let params: [String: Any]
}
How to make JSONDecoder combine all unknown key-value pairs into params property?
Decodable is incredibly powerful. It can decode completely arbitrary JSON, so this is just a sub-set of that problem. For a fully worked-out JSON Decodable, see this JSON.
I'll pull the concept of Key from example, but for simplicity I'll assume that values must be either Int or String. You could make parameters be [String: JSON] and use my JSON decoder instead.
struct Model: Decodable {
let name: String
let number: Int
let params: [String: Any]
// An arbitrary-string Key, with a few "well known and required" keys
struct Key: CodingKey, Equatable {
static let name = Key("name")
static let number = Key("number")
static let knownKeys = [Key.name, .number]
static func ==(lhs: Key, rhs: Key) -> Bool {
return lhs.stringValue == rhs.stringValue
}
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
// First decode what we know
name = try container.decode(String.self, forKey: .name)
number = try container.decode(Int.self, forKey:. number)
// Find all the "other" keys
let optionalKeys = container.allKeys
.filter { !Key.knownKeys.contains($0) }
// Walk through the keys and try to decode them in every legal way
// Throw an error if none of the decodes work. For this simple example
// I'm assuming it is a String or Int, but this is also solvable for
// arbitarily complex data (it's just more complicated)
// This code is uglier than it should be because of the `Any` result.
// It could be a lot nicer if parameters were a more restricted type
var p: [String: Any] = [:]
for key in optionalKeys {
if let stringValue = try? container.decode(String.self, forKey: key) {
p[key.stringValue] = stringValue
} else {
p[key.stringValue] = try container.decode(Int.self, forKey: key)
}
}
params = p
}
}
let json = Data("""
{
"name": "Some name",
"number": 42,
"param0": 1,
"param1": "2",
"param2": 3
}
""".utf8)
try JSONDecoder().decode(Model.self, from: json)
// Model(name: "Some name", number: 42, params: ["param0": 1, "param1": "2", "param2": 3])
ADDITIONAL THOUGHTS
I think the comments below are really important and future readers should look them over. I wanted to show how little code duplication is required, and how much of this can be easily extracted and reused, such that no magic or dynamic features are required.
First, extract the pieces that are common and reusable:
func additionalParameters<Key>(from container: KeyedDecodingContainer<Key>,
excludingKeys: [Key]) throws -> [String: Any]
where Key: CodingKey {
// Find all the "other" keys and convert them to Keys
let excludingKeyStrings = excludingKeys.map { $0.stringValue }
let optionalKeys = container.allKeys
.filter { !excludingKeyStrings.contains($0.stringValue)}
var p: [String: Any] = [:]
for key in optionalKeys {
if let stringValue = try? container.decode(String.self, forKey: key) {
p[key.stringValue] = stringValue
} else {
p[key.stringValue] = try container.decode(Int.self, forKey: key)
}
}
return p
}
struct StringKey: CodingKey {
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
Now, the decoder for Model is reduced to this
struct Model: Decodable {
let name: String
let number: Int
let params: [String: Any]
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringKey.self)
name = try container.decode(String.self, forKey: StringKey("name"))
number = try container.decode(Int.self, forKey: StringKey("number"))
params = try additionalParameters(from: container,
excludingKeys: ["name", "number"].map(StringKey.init))
}
}
It would be nice if there were some magic way to say "please take care of these properties in the default way," but I don't quite know what that would look like frankly. The amount of code here is about the same as for implementing NSCoding, and much less than for implementing against NSJSONSerialization, and is easily handed to swiftgen if it were too tedious (it's basically the code you have to write for init). In exchange, we get full compile-time type checking, so we know it won't crash when we get something unexpected.
There are a few ways to make even the above a bit shorter (and I'm currently thinking about ideas involving KeyPaths to make it even more convenient). The point is that the current tools are very powerful and worth exploring.

parse JSON with swift via a model to object - decode int/string trouble

To get JSON from a website and turn it into an object is fairly simple using swift 4:
class func getJSON(completionHandler: #escaping (MyModel) -> Void)
{
let myUrl = "https://example.com/whatever"
if let myUrl = URL(string: myUrl)
{
URLSession.shared.dataTask(with: myUrl)
{ (data, response, err) in
if let data = data
{
do
{
let myData = try JSONDecoder().decode(MyModel.self, from: data)
completionHandler(myData)
}
catch let JSONerr
{
print("Error: \(JSONerr)")
}
}
return
}.resume()
}
}
MyModel is a data model:
struct MyModel
{
products: [MyProduct]
}
struct MyProduct
{
id: Int
...
I use this to GET from my WebService and it works well for most JSON structures.
However, I facing trouble with this complex JSON object. (By complex I mean too long to post here, so I hope you can figure-out such a pattern. Also the JSON object it has many nested arrays, etc.)
Eg.
{
"products" : [
{
"name" : "abc"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
},
{
"name" : "xyzzy"
"colors" : [
{
"outside" : [
{
"main" : "blue"
}]
}]
"id" :
...
}]
}
(This may not be valid JSON - it is a simple extract of a larger part.)
The app crashes "...Expected to decode String but found a number
instead."!
So I change the model to use a 'String' in place of the
'Int'.
Again it crashes, saying "...Expected to decode Int but found
a string/data instead."!
So I change the model back, place an 'Int'
in place of the 'String'.
(The cycle repeats.)
It seems the value in question is sometimes an Int and sometimes a
String.
This NOT only happens with a certain key. I know of at least five other similar cases in this JSON.
So that means that I may get another error for another key, if a solution was only for that specific key. I would not be surprised to find many other cases as well.
QUESTION: How can I properly decode the JSON to my object, where the type of its elements can be either an Int or a String?
I want a solution that will either apply to all Model members or try convert a value to a String type if Int fails. Since I don't know which other keys will as fail.
You can use if lets to handle unpredictable values:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let memberValue = try? container.decode([String].self, forKey: .member){
stringArrayMember = memberValue
}
else if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
intMember = int
}
}
Or if it's a specific case of String vs Int and you'd like the same variable to handle the values, then something like:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
if let str = try? container.decode(String.self, forKey: .member){
stringMember = str
}
else if let int = try? container.decode(Int.self, forKey: .member){
stringMember = String(int)
}
}
Edit
Your MyProduct will now look like:
struct MyProduct: Decodable {
var id: String?
var someOtherProperty: String?
enum MemberKeys: String, CodingKey {
case id
case someOtherProperty
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: MemberKeys.self)
someOtherProperty = try? container.decode(String.self, forKey: .someOtherProperty)
// Problematic property which can be String/Int
if let str = try? container.decode(String.self, forKey: .id){
id = str
}
else if let int = try? container.decode(Int.self, forKey: .id){
id = String(int)
}
}
}
Hope this helps.
This wasn't the problem that the error message gave!
All I needed to do to fix the problem was to employ CodingKeys.
I was hoping to avoid this since the data structure (JSON) had lots of members. But this fixed the problem.
Now an example of my model:
struct Product
{
let name: String
let long_name_value: String
...
enum MemberKeys: String, CodingKey
{
case name
case longNameValue = "long_name_value"
...
}
}
I guess the reason is swift doesn't like snake case (eg. "long_name_value"), so I needed to convert it to camel case (eg."longNameValue"). Then the errors disappeared.

Swift 4 JSON Parsing Int - what am I doing wrong?

Swift 4 JSON Parsing Int issue. All the examples I've seen Int is encoded / decoded out of the box. Anyone see what I am doing wrong?
Thanks!
JSON
let patientBundleEntry = """
{
"resourceType": "Bundle",
"id": "patientListBundle",
"type": "SearchSet",
"total": 123
}
"""
Classes
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int? // this is printing -> Optional(105553116787496) instead of 123
}
class Resource:Codable {
var resourceType:ResourceType? // this is printing fine
}
Test - my assert for total being 123 fails and the optional is a long number. Any ideas why? Is my encoding wrong using .utf8??
func testModelBundle(){
let jsonDataEncoded:Data? = patientBundleEntry.data(using: .utf8)!
guard let responseData = jsonDataEncoded else {
print("Error: did not receive data")
}
do {
let bundleDecoded = try JSONDecoder().decode(BundleFHIR.self, from: responseData)
print("bundleDecoded.resourceType resource type \(bundleDecoded.resourceType )") //THIS is right
print("bundleDecoded.resourceType total \(bundleDecoded.total )") THIS is wrong
assert(bundleDecoded.total == 123, "something is wrong") // ***** <- this assert fails and it prints Optional(105553116787496)
} catch {
print("error trying to convert data to JSON")
}
}
At first you have to decode and then parse the JSON data.
Follow the below code :
struct Patient: Codable {
var resourceType: String
var id: String
var type: String
var total: Int
}
let json = patientBundleEntry.data(using: .utf8)!
let decoder = JSONDecoder()
let patient = try! decoder.decode(Patient.self, from: json)
print(patient.total) // Prints - 123
Ok there were a lot of issues with my code. Mainly that I didn't have codingKeys as private and therefore tried to rename it because the inheritance tree could not discern between the two. This caused me to not be implementing the true protocol. Not sure why it was half working... but here is my final code and it works great!
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int?
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
total = try values.decodeIfPresent(Int.self, forKey: .total)
entry = try values.decodeIfPresent([BundleEntry].self, forKey: .entry)
}
private enum CodingKeys: String, CodingKey
{
case total
case entry
}
}
class Resource:Codable {
var resourceType:ResourceType?
var id:String?
init(){
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
resourceType = try values.decode(ResourceType.self, forKey: .resourceType)
id = try values.decode(String.self, forKey: .id)
}
private enum CodingKeys: String, CodingKey
{
case resourceType
case id
}
}