Swift Codable: subclass JSONDecoder for custom behavior - json

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)

Related

How to fix? Expected to decode Dictionary<String, Any> but found a string/data instead

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)
...

Decode a nested object in Swift 5 with custom initializer

I have an API which returns a payload like this (just one item is included in the example).
{
"length": 1,
"maxPageLimit": 2500,
"totalRecords": 1,
"data": [
{
"date": "2021-05-28",
"peopleCount": 412
}
]
}
I know I can actually create a struct like
struct Root: Decodable {
let data: [DailyCount]
}
struct DailyCount: Decodable {
let date: String
let peopleCount: Int
}
For different calls, the same API returns the same format for the root, but the data is then different. Moreover, I do not need the root info (length, totalRecords, maxPageLimit).
So, I am considering to create a custom init in struct DailyCount so that I can use it in my URL session
let reports = try! JSONDecoder().decode([DailyCount].self, from: data!)
Using Swift 5 I tried this:
struct DailyCount: Decodable {
let date: String
let peopleCount: Int
}
extension DailyCount {
enum CodingKeys: String, CodingKey {
case data
enum DailyCountCodingKeys: String, CodingKey {
case date
case peopleCount
}
}
init(from decoder: Decoder) throws {
// This should let me access the `data` container
let container = try decoder.container(keyedBy: CodingKeys.self
peopleCount = try container.decode(Int.self, forKey: . peopleCount)
date = try container.decode(String.self, forKey: .date)
}
}
Unfortunately, it does not work. I get two problems:
The struct seems not to conform anymore to the Decodable protocol
The CodingKeys does not contain the peopleCount (therefore returns an error)
This can’t work for multiple reasons. You are trying to decode an array, so your custom decoding implementation from DailyCount won’t be called at all (if it were to compile) since at the top level your JSON contains an object, not an array.
But there is a much simpler solution which doesn’t even require implementing Decodable yourself.
You can create a generic wrapper struct for your outer object and use that with whatever payload type you need:
struct Wrapper<Payload: Decodable>: Decodable {
var data: Payload
}
You then can use this to decode your array of DailyCount structs:
let reports = try JSONDecoder().decode(Wrapper<[DailyCount]>.self, from: data).data
This can be made even more transparent by creating an extension on JSONDecoder:
extension JSONDecoder {
func decode<T: Decodable>(payload: T.Type, from data: Data) throws -> T {
try decode(Wrapper<T>.self, from: data).data
}
}
Sven's answer is pure and elegant, but I would be remiss if I didn't point out that there is also a stupid but easy way: dumpster-dive into the "data" without using Codable at all. Example:
// preconditions
let json = """
{
"length": 1,
"maxPageLimit": 2500,
"totalRecords": 1,
"data": [
{
"date": "2021-05-28",
"peopleCount": 412
}
]
}
"""
let jsonData = json.data(using: .utf8)!
struct DailyCount: Decodable {
let date: String
let peopleCount: Int
}
// okay, here we go
do {
let dict = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [AnyHashable:Any]
let arr = dict?["data"] as? Array<Any>
let json2 = try JSONSerialization.data(withJSONObject: arr as Any, options: [])
let output = try JSONDecoder().decode([DailyCount].self, from: json2)
print(output) // yep, it's an Array of DailyCount
} catch {
print(error)
}

Swift conditional conformance with two cases

The short question is how can I make a type conditionally conform to a protocol with two conditions when either one of them is met?
I have a generic type NetworkResponse<Data>. It represents server response. Here is how it is defined:
enum NetworkResponse<Data> {
case success(Data)
case error(ServerError)
}
I want to make NetworkResponse to conform Decodable. Here is my server response format:
{
"data": {
"someKey": "someValue",
"anotherKey": 15
},
"meta": {
"returnCode": 0,
"returnMessage": "operation is successful"
}
}
The data part depends on what request is made. The meta part represents some meta data about response. Like whether it is success or if not what is the error.
So here is how I implemented Decodable:
extension NetworkResponse: Decodable where Data: Decodable {
enum CodingKeys: CodingKey {
case meta
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let metaValue = try container.decode(ServerError.self, forKey: .meta)
if metaValue.code != 0 {
self = .error(metaValue)
} else {
self = .success(try container.decode(Data.self, forKey: .data))
}
}
}
So far so good. But here is my problem. For some apis which don't need to return any data the data part in response is omitted. In this case my response would look like this:
{
"meta": {
"returnCode": 0,
"returnMessage": "operation is successful"
}
}
In this case I want to decode the response json as NetworkResponse<Void>. But since Void can not conform to Decodable (since it is non nominal type) compiler gives error.
To overcome this I tried to create more specialized extension of Decodable where Data is Void like this:
extension NetworkResponse: Decodable where Data == Void {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let metaValue = try container.decode(AppErrors.Server.self, forKey: CodingKeys.meta)
if metaValue.code != 0 {
self = .error(metaValue)
} else {
self = .success(())
}
}
}
But still compiler complines like: Conflicting conformance of 'NetworkResponse<Data>' to protocol 'Decodable'; there cannot be more than one conformance, even with different conditional bounds.
So how can I create seperate init(from:) function that is used for when Data is Void?
I strongly urge you to change the generic parameter's type name away from Data since it's too easily confused with Foundation.Data which is widely used throughout Swift.
On to the problem itself, you can make an empty struct to represent "void" and add a new case to your NetworkResponse:
struct EmptyData: Decodable {}
enum NetworkResponse<T> {
case success(T)
case successWithEmptyData
case error(ServerError)
}
extension NetworkResponse: Decodable where T: Decodable {
private enum CodingKeys: CodingKey {
case meta
case data
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let metaValue = try container.decode(ServerError.self, forKey: .meta)
if metaValue.code != 0 {
self = .error(metaValue)
} else if T.self == EmptyData.self {
self = .successWithEmptyData
} else {
self = .success(try container.decode(T.self, forKey: .data))
}
}
}
let response = try! JSONDecoder().decode(NetworkResponse<EmptyData>.self, from: jsonData)
Alternatively, you can make your .success case contains a T? when you expect no data in return and decode accordingly.

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 - convert json type Int to String

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)")
}
}
}