I'm trying to practice Swift's Codable API.
I send a network request and I receive one single line each time as follows where I have to deal with dynamic keys :
Response example 1:
{
"EUR": 4695.01
}
Response example 2:
{
"USD": 479.01
}
Response example 3:
{
"BTC": 4735.01
}
I tried this method to parse the dynamic keys :
struct ConversionResponseModel: Decodable {
typealias destinationCurrency = String
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
}
}
and my fetching request :
do {
let myResult = try JSONDecoder().decode(ConversionResponseModel.self, from: data)
print(myResult)
} catch {
print(error)
}
But I get this as a result : ConversionResponseModel(), but not the currency values.
It sounds like I'm missing something. Any help please. Thank you
You're almost there. The JSON you're getting will return a dictionary of [String:Double]. You can then covert that using:
struct ConversionResponseModel: Decodable {
typealias DestinationCurrency = String
let currency : DestinationCurrency
let value : Double
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let dict = try container.decode([String:Double].self)
guard let key = dict.keys.first else {
throw NSError(domain: "Decoder", code: 0, userInfo: [:])
}
currency = key
value = dict[key] ?? -1
}
}
Note: taking into account Rob Napier's comment, you could substitute Decimal for Double -- see his comment on the original question for more detail
Related
What is wrong here? Or how else I should decode, I would NOT use JSONSerialize.
let jsonData = try! Data(contentsOf: urls[0])
let decoder = JSONDecoder()
let d = try decoder.decode([String: JSON].self, from: jsonData)
file content is a simple JSON:
{"name": "fff", "price": 10}
And my JSON code:
public enum JSON: Decodable {
case string(String)
case number(Float)
case object([String:JSON])
case array([JSON])
case bool(Bool)
}
You need to add a custom init(from:) where you try to decode into each possible enum case until you are successful or throw an error
Here is a short version that handles three of the cases
struct EnumDecoderError: Error {}
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = JSON.string(string)
} else if let number = try? container.decode(Float.self) {
self = JSON.number(number)
} else if let array = try? container.decode([JSON].self) {
self = JSON.array(array)
} else {
throw EnumDecoderError()
}
}
as mentioned in the comments by #LeoDabus we can catch typeMismatch errors (and throw any other error directly) or as before throw an error at the end if no decoding worked. (Again a shortened version)
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
let string = try container.decode(String.self)
self = JSON.string(string)
} catch DecodingError.typeMismatch {
do {
let number = try container.decode(Float.self)
self = JSON.number(number)
} catch DecodingError.typeMismatch {
do {
let array = try container.decode([JSON].self)
self = JSON.array(array)
} catch DecodingError.typeMismatch {
throw DecodingError.typeMismatch(JSON.self, .init(codingPath: decoder.codingPath, debugDescription: "Data type is not supported"))
}
}
}
}
First of all you don't need to maintain datatypes in the JSON enum to parse your response.
The JSONDecoder will be able to parse with the appropriate datatype if you match your object to the response structure that you receive from APIs or JSON files maintained locally
Taking you json file as example:
{"name": "fff", "price": 10}
The recommended way to parse this structure would be as follows
Create a struct or class according to your needs. For this I will use a struct
struct Product: Decodable {
var name: String?
var price: Int?
}
I have marked both the vars optionals just in case to handle the failures if the field does not exist in the JSON response.
Parse the Structure
Use the product struct that was created in the previous step by creating a decoder instance and setting the Product.Self to parse the object
let decoder = JSONDecoder()
let productObject = try decoder.decode(Product.self, from: jsonData)
If you have an array of objects of the same structure in the JSON response use below:
let productObjects = try decoder.decode([Product].self, from: jsonData)
Just include [] around the product object
You need to decode it into a structure
private struct MyData: Codable {
var name: String?
var price: Int?
}
...
let jsonData = try! Data(contentsOf: urls[0])
let d = try JSONDecoder().decode(MyData.self, from: jsonData)
...
I'm having an inconsistent API that might return either a String or an Number as a part of the JSON response.
The dates also could be represented the same way as either a String or a Number, but are always an UNIX timestamp (i.e. timeIntervalSince1970).
To fix the issue with the dates, I simply used a custom JSONDecoder.DateDecodingStrategy:
decoder.dateDecodingStrategy = JSONDecoder.DateDecodingStrategy.custom({ decoder in
let container = try decoder.singleValueContainer()
if let doubleValue = try? container.decode(Double.self) {
return Date(timeIntervalSince1970: doubleValue)
} else if let stringValue = try? container.decode(String.self),
let doubleValue = Double(stringValue) {
return Date(timeIntervalSince1970: doubleValue)
}
throw DecodingError.dataCorruptedError(in: container,
debugDescription: "Unable to decode value of type `Date`")
})
However, no such customization is available for the Int or Double types which I'd like to apply it for.
So, I have to resort to writing Codable initializers for each of the model types that I'm using.
The alternative approach I'm looking for is to subclass the JSONDecoder and override the decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable method.
In that method I'd like to "inspect" the type T that I'm trying to decode to and then, if the base implementation (super) fails, try to decode the value first to String and then to the T (the target type).
So far, my initial prototype looks like this:
final class CustomDecoder: JSONDecoder {
override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
do {
return try super.decode(type, from: data)
} catch {
if type is Int.Type {
print("Trying to decode as a String")
if let decoded = try? super.decode(String.self, from: data),
let converted = Int(decoded) {
return converted as! T
}
}
throw error
}
}
}
However, I found out that the "Trying to decode as a String" message is never printed for some reason, even though the control reaches the catch stage.
I'm happy to have that custom path only for Int and Double types, since the T is Codable and that doesn't guarantee ability to initialize a value with the String, however, I of course welcome a more generalized approach.
Here's the sample Playground code that I came up with to test my prototype. It can be copy-pasted directly into the Playground and works just fine.
My goal is to have both jsonsample1 and jsonsample2 to produce the same result.
import UIKit
final class CustomDecoder: JSONDecoder {
override func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
do {
return try super.decode(type, from: data)
} catch {
if type is Int.Type {
print("Trying to decode as a String")
if let decoded = try? super.decode(String.self, from: data),
let converted = Int(decoded) {
return converted as! T
}
}
throw error
}
}
}
let jsonSample1 =
"""
{
"name": "Paul",
"age": "38"
}
"""
let jsonSample2 =
"""
{
"name": "Paul",
"age": 38
}
"""
let data1 = jsonSample1.data(using: .utf8)!
let data2 = jsonSample2.data(using: .utf8)!
struct Person: Codable {
let name: String?
let age: Int?
}
let decoder = CustomDecoder()
let person1 = try? decoder.decode(Person.self, from: data1)
let person2 = try? decoder.decode(Person.self, from: data2)
print(person1 as Any)
print(person2 as Any)
What could be the reason for my CustomDecoder not working?
The primary reason that your decoder doesn't do what you expect is that you're not overriding the method that you want to be: JSONDecoder.decode<T>(_:from:) is the top-level method that is called when you call
try JSONDecoder().decode(Person.self, from: data)
but this is not the method that is called internally during decoding. Given the JSON you show as an example, if we write a Person struct as
struct Person: Decodable {
let name: String
let age: Int
}
then the compiler will write an init(from:) method which looks like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
age = try container.decode(Int.self, forKey: .age)
}
Note that when we decode age, we are not calling a method on the decoder directly, but on a KeyedCodingContainer that we get from the decoder — specifically, the Int.Type overload of KeyedDecodingContainer.decode(_:forKey:).
In order to hook into the methods that are called during decode at the middle levels of a Decoder, you'd need to hook into its actual container methods, which is very difficult — all of JSONDecoder's containers and internals are private. In order to do this by subclassing JSONDecoder, you'd end up needing to pretty much reimplement the whole thing from scratch, which is significantly more complicated than what you're trying to do.
As suggested in a comment, you're likely better off either:
Writing Person.init(from:) manually by trying to decode both Int.self and String.self for the .age property and keeping whichever one succeeds, or
If you need to reuse this solution across many types, you can write a wrapper type to use as a property:
struct StringOrNumber: Decodable {
let number: Double
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
number = try container.decode(Double.self)
} catch (DecodingError.typeMismatch) {
let string = try container.decode(String.self)
if let n = Double(string) {
number = n
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value wasn't a number or a string...")
}
}
}
}
struct Person: Decodable {
let name: String
let age: StringOrNumber
}
You can also write StringOrNumber as an enum which can hold either case string(String) or case number(Double) if knowing which type of value was in the payload was important:
enum StringOrNumber: Decodable {
case number(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try .number(container.decode(Double.self))
} catch (DecodingError.typeMismatch) {
let string = try container.decode(String.self)
if let n = Double(string) {
self = .string(string)
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Value wasn't a number or a string...")
}
}
}
}
Though this isn't as relevant if you always need Double/Int access to the data, since you'd need to re-convert at the use site every time (and you call this out in a comment)
Need some help with more complicated json, with the newest swift4.1 encoder/decoder:
struct:
struct LMSRequest: Decodable {
let id : Int?
let method : String?
let params : [String]?
enum CodingKeys: String, CodingKey {
case id = "id"
case method = "method"
case params = "params"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(Int.self, forKey: .id)
method = try values.decodeIfPresent(String.self, forKey: .method)
params = try values.decodeIfPresent([String].self, forKey: .params)
}}
json:
let json = """
{
"id": 1,
"method": "slim.request",
"params": [
"b8:27:eb:db:6d:62",
[
"serverstatus",
"-",
1,
"tags:GPASIediqtymkovrfijnCYXRTIuwxNlasc"
]
]
}
""".data(using: .utf8)!
code:
let decoder = JSONDecoder()
let lms = try decoder.decode(LMSRequest.self, from: json)
print(lms)
Error is expected to decode string but found array instead. It's coming from the nested array within the "params" array... really stuck on how to build this out, Thanks!
Given what you've described, you should store params as an enum like this:
enum Param: CustomStringConvertible {
case string(String)
case int(Int)
case array([Param])
var description: String {
switch self {
case let .string(string): return string
case let .int(int): return "\(int)"
case let .array(array): return "\(array)"
}
}
}
A param can either be a string, an int, or an array of more params.
Next, you can make Param Decodable by trying each option in turn:
extension Param: Decodable {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let string = try? container.decode(String.self) {
self = .string(string)
} else if let int = try? container.decode(Int.self) {
self = .int(int)
} else {
self = .array(try container.decode([Param].self))
}
}
}
Given this, there's no need for custom decoding logic in LMSRequest:
struct LMSRequest: Decodable {
let id : Int?
let method : String?
let params : [Param]?
}
As a side note, I would carefully consider whether these fields are all truly optional. It's very surprising that id is optional, and quite surprising that method is optional, and slightly surprising that params are optional. If they're not really optional, don't make them optional in the type.
From your comments, you're probably misunderstanding how to access enums. params[1] is not a [Param]. It's an .array([Param]). So you have to pattern match it since it might have been a string or an int.
if case let .array(values) = lms.params[1] { print(values[0]) }
That said, if you're doing this a lot, you can make this simpler with extensions on Param:
extension Param {
var stringValue: String? { if case let .string(value) = self { return value } else { return nil } }
var intValue: Int? { if case let .int(value) = self { return value } else { return nil } }
var arrayValue: [Param]? { if case let .array(value) = self { return value } else { return nil } }
subscript(_ index: Int) -> Param? {
return arrayValue?[index]
}
}
With that, you can say things like:
let serverstatus: String? = lms.params[1][0]?.stringValue
Which is probably closer to what you had in mind. (The : String? is just to be clear about the returned type; it's not required.)
For a more complex and worked-out example of this approach, see my generic JSON Decodable that this is a subset of.
I'm calling an API and storing the data in an array.
But if there is no data the debugger says:
Expected to decode Array but found a dictionary instead.
The fail JSON response is:
'{"status":"Failed","msg":"Sorry It\'s Not Working"}'
The success JSON response is:
'[{"id":"509","name":"TEC TEST !"#!12","sortingId":"1"},
{"id":"510","name":"TEC TEST !"#!12","sortingId":"2"},
{"id":"511","name":"TEC TEST !"#!12","sortingId":"3"},
{"id":"512","name":"TEC TEST !"#!12","sortingId":"4"},
{"id":"513","name":"TEC TEST !"#!12","sortingId":"5"},
{"id":"514","name":"TEC TEST !"#!12","sortingId":"6"},
{"id":"519","name":"TEC TEST !"#!12","sortingId":"7"}]'
So I want to switch between fetching my response as
var result:[Items]?
and
var result:Items?
if the Failed JSON gets send
I've been google´ing and searching Stackoverflow without luck
Is there an solution to say if JSON is an array or dictionary?
My Struct:
struct Items: Codable {
let id: String?
let sortingId: String?
let name: String?
let response: String?
let status: String?
let msg: String?
}
My processing of the response:
var result:[Items]?
result = try JSONDecoder().decode([Items].self, from: data!)
DispatchQueue.main.async {
for item in result! {
self.itemArray.append((name: item.name!, id: Int(item.id!)!, sortingId: Int(item.sortingId!)!))
}
}
One solution is to write a custom initializer which conditionally decodes the array or the dictionary.
Please ask the owner of the service to send more consistent JSON. It's very bad. At least the object should be always a dictionary with key status and either the array for key items or the key msg.
This code first tries to decode the array with unkeyedContainer. If it fails it decodes the dictionary.
struct Item: Decodable {
let id: String
let sortingId: String
let name: String
}
struct ItemData : Decodable {
private enum CodingKeys: String, CodingKey { case status, msg }
let status : String?
let msg: String?
var items = [Item]()
init(from decoder: Decoder) throws {
do {
var unkeyedContainer = try decoder.unkeyedContainer()
while !unkeyedContainer.isAtEnd {
items.append(try unkeyedContainer.decode(Item.self))
}
status = nil; msg = nil
} catch DecodingError.typeMismatch {
let container = try decoder.container(keyedBy: CodingKeys.self)
status = try container.decodeIfPresent(String.self, forKey: .status)
msg = try container.decodeIfPresent(String.self, forKey: .msg)
}
}
}
And call it
result = try JSONDecoder().decode(ItemData.self, from: data!)
A – probably more suitable – alternative is to catch the error in the JSONDecoder().decode line and use two simple structs
struct Item: Decodable {
let id: String
let sortingId: String
let name: String
}
struct ErrorData : Decodable {
let status : String
let msg: String
}
and call it
do {
let decoder = JSONDecoder()
do {
let result = try decoder.decode([Item].self, from: data!)
print(result)
} catch DecodingError.typeMismatch {
let result = try decoder.decode(ErrorData.self, from: data!)
print(result)
}
} catch { print(error) }
A big benefit is that all properties can be non-optional.
First of all the JSON does not contain any array. It's very very easy to read JSON. There are only 2 (two!) collection types, array [] and dictionary {}. As you can see there are no square brackets at all in the JSON string.
it will helpful
I have json data like this code below:
{
"all": [
{
"ModelId": 1,
"name": "ghe",
"width": 2
},
{
"ModelId": 2,
"name": "ban",
"width": 3
}]
}
I try to get the modelId and convert it to String but it's not working with my code:
let data = NSData(contentsOf: URL(string: url)!)
do {
if let data = data, let json = try JSONSerialization.jsonObject(with: data as Data) as? [String: Any], let models = json["all"] as? [[String:Any]] {
for model in models {
if let name = model["ModelId"] as? String {
_modelList.append(name)
}
}
}
completion(_modelList)
}catch {
print("error")
completion(nil)
}
How to fix this issue? Thanks.
I think ModelId is integer type. So, can you try to cast it to Integer
for model in models {
if let name = model["ModelId"] as? Int{
_modelList.append("\(name)")
}
}
Hope, it will help you.
if let as? is to unwrap, not type casting. So you unwrap first, then you cast it into string.
for model in models {
if let name = model["ModelId"] as? Int {
_modelList.append("\(name)")
}
}
Currently you are looking for a wrong key ,
for model in models {
if let name = model["ModelId"] as? NSNumber {
_modelList.append(name.stringValue)
}
}
As long as you are using JSONSerialization.jsonObject to parse your JSON you have very little control over the type the deserialiser will create, you basically let the parser decide. Sensible as it is it will create "some kind of " NSNumber of an Int type from a number without quotes. This can not be cast to a String, therefore your program will fail.
You can do different things in order to "fix" this problem, I would like to suggest the Codable protocol for JSON-parsing, but this specific problem can probably only be solved using a custom initialiser which looks kind of verbose as can be seen in this question.
If you just want to convert your NSNumber ModelId to a String you will have to create a new object (instead of trying to cast in vain). In your context this might simply be
if let name = String(model["ModelId"]) { ...
This is still not an elegant solution, however it will solve the problem at hand.
another approach is:
import Foundation
struct IntString: Codable
{
var value: String = "0"
init(from decoder: Decoder) throws
{
// get this instance json value
let container = try decoder.singleValueContainer()
do
{
// try to parse the value as int
value = try String(container.decode(Int.self))
}
catch
{
// if we failed parsing the value as int, try to parse it as a string
value = try container.decode(String.self)
}
}
func encode(to encoder: Encoder) throws
{
var container = encoder.singleValueContainer()
try container.encode(value)
}
}
my solution is to create a new struct that will be able to receive either a String or and Int and parse it as a string, this way in my code i can decide how to treat it, and when my server sends me sometimes an Int value and sometimes a json with that same key as a String value - the parser can parse it without failing
of course you can do it with any type (date / double / float / or even a full struct), and even insert it with some logic of your own (say get the string value of an enum based on the received value and use it as index or whatever)
so your code should look like this:
import Foundation
struct Models: Codable {
let all: [All]
}
struct All: Codable {
let modelID: IntString
let name: String
let width: IntString
enum CodingKeys: String, CodingKey {
case modelID = "ModelId"
case name = "name"
case width = "width"
}
}
parse json into the Models struct:
let receivedModel: Decodable = Bundle.main.decode(Models.self, from: jsonData!)
assuming you'r json decoder is:
import Foundation
extension Bundle
{
func decode<T: Decodable>(_ type: T.Type, from jsonData: Data, dateDecodingStrategy: JSONDecoder.DateDecodingStrategy = .deferredToDate, keyDecodingStrategy: JSONDecoder.KeyDecodingStrategy = .useDefaultKeys) -> T
{
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = dateDecodingStrategy
decoder.keyDecodingStrategy = keyDecodingStrategy
do
{
return try decoder.decode(T.self, from: jsonData)
}
catch DecodingError.keyNotFound(let key, let context)
{
fatalError("Failed to decode \(jsonData) from bundle due to missing key '\(key.stringValue)' not found – \(context.debugDescription)")
}
catch DecodingError.typeMismatch(let type, let context)
{
print("Failed to parse type: \(type) due to type mismatch – \(context.debugDescription) the received JSON: \(String(decoding: jsonData, as: UTF8.self))")
fatalError("Failed to decode \(jsonData) from bundle due to type mismatch – \(context.debugDescription)")
}
catch DecodingError.valueNotFound(let type, let context)
{
fatalError("Failed to decode \(jsonData) from bundle due to missing \(type) value – \(context.debugDescription)")
}
catch DecodingError.dataCorrupted(_)
{
fatalError("Failed to decode \(jsonData) from bundle because it appears to be invalid JSON")
}
catch
{
fatalError("Failed to decode \(jsonData) from bundle: \(error.localizedDescription)")
}
}
}