Swift conditional conformance with two cases - json

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.

Related

Swift Codable: subclass JSONDecoder for custom behavior

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)

Adding a new property to a model: converting existing JSON

say I had this model:
struct MyModel: Codable {
var name: String
}
By already using this property, I persisted JSON files like this:
{"name":"Foo"}
Now, I want to add a new property uuid, but want to make sure to have a valid migration path to locally persisted existing data. So, I rewrote my struct to this:
struct MyModel {
let uuid: UUID
var name: String
}
extension MyModel: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.uuid = try container.decode(UUID.self, forKey: .uuid)
} catch {
self.uuid = UUID()
}
self.name = try container.decode(String.self, forKey: .name)
}
enum CodingKeys: String, CodingKey {
case uuid
case name
}
}
My thought process here is to catch the error when not finding the property in the JSON by instantiating it manually when needed.
This leads to the error The data couldn’t be read because it is missing..
I might have to add that to be able to load the locally persisted data, I have built the function ATPersistLocally.loadAll(of:) in a personal toolbox package (that I can add to my projects with SPM), that looks like this:
public func loadAll<T: Codable & Identifiable>(of type: T.Type) -> [T] {
var result = [T]()
let pathFolder = docPath.appendingPathComponent(String(describing: type))
if fileManager.fileExists(atPath: pathFolder.path) {
var urls = [URL]()
do {
urls = try fileManager.contentsOfDirectory(at: pathFolder, includingPropertiesForKeys: nil, options: .skipsHiddenFiles)
} catch {
ATLogger.shared.logToConsole(message: "Could not load content of directory: \(error.localizedDescription)", type: .error)
}
for url in urls {
if let data = fileManager.contents(atPath: url.path) {
do {
let instance = try decoder.decode(T.self, from: data)
result.append(instance)
} catch {
ATLogger.shared.logToConsole(message: "Could not decode data: \(error.localizedDescription)", type: .error)
}
}
}
}
return result
}
Any hints on how to resolve this?
Simple solution: Omit the CodingKey uuid to decode only name (and you can omit the init method, too).
struct MyModel {
let uuid = UUID()
var name: String
}
extension MyModel: Codable {
enum CodingKeys: String, CodingKey { case name }
}
Important note:
Never print error.localizedDescription in a Decoding catch block, print always only the error instance to get the actual descriptive error message.
Use decodeIfPresentinstead in init(from decoder: Decoder) together with a local variable
if let uuid = try container.decodeIfPresent(UUID.self, forKey: .uuid) {
self.uuid = uuid
} else {
self.uuid = UUID()
}

Swift 4.1 Codable/Decodable Nested Array

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.

Swift 4 Decodable multiple containers

I'm trying to understand how could I parse this multiple container JSON to an object. I've tried this approach (Mark answer), but he explain how to solve it using one-level container. For some reason I can't mimic the behaviour for multiple containers.
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql // The top level "user" key
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let post = try values.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}
I'm getting:
Swift.DecodingError.Context(codingPath: [], debugDescription: "Cannot get KeyedDecodingContainer<PostKeys> -- no value found for key \"shortcode_media\"", underlyingError: nil))
Any help will be much appreciated, thank you!
As vadian notes, you haven't matched the JSON structure. There is no shortcode_media key at the top level like you've encoded in CodingKeys.
In order to decode this with a custom decoder, you will need to walk through each level and deal with it.
class Post: Decodable {
enum CodingKeys: String, CodingKey {
case graphql
}
enum GraphQLKeys: String, CodingKey {
case shortcode_media
}
enum PostKeys: String, CodingKey {
case id
}
required init(from decoder: Decoder) throws {
// unload the top level
let container = try decoder.container(keyedBy: CodingKeys.self)
// Unload the graphql key
let graphql = try container.nestedContainer(keyedBy: GraphQLKeys.self, forKey: .graphql)
// unload the shortcode_media key
let post = try graphql.nestedContainer(keyedBy: PostKeys.self, forKey: .shortcode_media)
// Finally, unload the actual object
self.id = try post.decode(String.self, forKey: .id)
}
var id: String
}
Please read the JSON.
Any opening { is quasi a separator. The indentation of the JSON indicates also the hierarchy.
For clarity I removed all coding keys and left the variable names – which should be camelCased – unchanged.
struct Root : Decodable {
let graphql : Graph
// to access the `Media` object declare a lazy instantiated property
lazy var media : Media = {
return graphql.shortcode_media
}()
}
struct Graph : Decodable {
let shortcode_media : Media
}
struct Media : Decodable {
let id: String
let shortcode : String
}
let jsonString = """
{
"graphql": {
"shortcode_media": {
"id": "1657677004214306744",
"shortcode": "BcBQHPchwe4"
}
}
}
"""
do {
let data = Data(jsonString.utf8)
var result = try decoder.decode(Root.self, from: data)
print(result.media)
} catch {
print("error: ", error)
}
Writing a custom initializer with nestedContainer is more effort than creating the actual hierarchy.
Please paste the entire code in a Playground and check it out.

Swift structures: handling multiple types for a single property

I am using Swift 4 and trying to parse some JSON data which apparently in some cases can have different type values for the same key, e.g.:
{
"type": 0.0
}
and
{
"type": "12.44591406"
}
I am actually stuck with defining my struct because I cannot figure out how to handle this case because
struct ItemRaw: Codable {
let parentType: String
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
throws "Expected to decode String but found a number instead.", and naturally,
struct ItemRaw: Codable {
let parentType: Float
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
throws "Expected to decode Float but found a string/data instead." accordingly.
How can I handle this (and similar) cases when defining my struct?
I ran into the same issue when trying to decode/encode the "edited" field on a Reddit Listing JSON response. I created a struct that represents the dynamic type that could exist for the given key. The key can have either a boolean or an integer.
{ "edited": false }
{ "edited": 123456 }
If you only need to be able to decode, just implement init(from:). If you need to go both ways, you will need to implement encode(to:) function.
struct Edited: Codable {
let isEdited: Bool
let editedTime: Int
// Where we determine what type the value is
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Check for a boolean
do {
isEdited = try container.decode(Bool.self)
editedTime = 0
} catch {
// Check for an integer
editedTime = try container.decode(Int.self)
isEdited = true
}
}
// We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try isEdited ? container.encode(editedTime) : container.encode(false)
}
}
Inside my Codable class, I then use my struct.
struct Listing: Codable {
let edited: Edited
}
Edit: A more specific solution for your scenario
I recommend using the CodingKey protocol and an enum to store all the properties when decoding. When you create something that conforms to Codable the compiler will create a private enum CodingKeys for you. This lets you decide on what to do based on the JSON Object property key.
Just for example, this is the JSON I am decoding:
{"type": "1.234"}
{"type": 1.234}
If you want to cast from a String to a Double because you only want the double value, just decode the string and then create a double from it. (This is what Itai Ferber is doing, you would then have to decode all properties as well using try decoder.decode(type:forKey:))
struct JSONObjectCasted: Codable {
let type: Double?
init(from decoder: Decoder) throws {
// Decode all fields and store them
let container = try decoder.container(keyedBy: CodingKeys.self) // The compiler creates coding keys for each property, so as long as the keys are the same as the property names, we don't need to define our own enum.
// First check for a Double
do {
type = try container.decode(Double.self, forKey: .type)
} catch {
// The check for a String and then cast it, this will throw if decoding fails
if let typeValue = Double(try container.decode(String.self, forKey: .type)) {
type = typeValue
} else {
// You may want to throw here if you don't want to default the value(in the case that it you can't have an optional).
type = nil
}
}
// Perform other decoding for other properties.
}
}
If you need to store the type along with the value, you can use an enum that conforms to Codable instead of the struct. You could then just use a switch statement with the "type" property of JSONObjectCustomEnum and perform actions based upon the case.
struct JSONObjectCustomEnum: Codable {
let type: DynamicJSONProperty
}
// Where I can represent all the types that the JSON property can be.
enum DynamicJSONProperty: Codable {
case double(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Decode the double
do {
let doubleVal = try container.decode(Double.self)
self = .double(doubleVal)
} catch DecodingError.typeMismatch {
// Decode the string
let stringVal = try container.decode(String.self)
self = .string(stringVal)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .double(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
One simple solution is to provide an implementation of init(from:) which attempts to decode the value as a String, and if that fails because the type is wrong, attempt to decode as a Double:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.parentType = try container.decode(String.self, forKey: .parentType)
} catch DecodingError.typeMismatch {
let value = try container.decode(Double.self, forKey: .parentType)
self.parentType = "\(value)"
}
}
I had to decode PHP/MySQL/PDO double value that is given as an String, for this use-case I had to extend the KeyedDecodingContainer, like so:
extension KeyedDecodingContainer {
func decode(forKey key: KeyedDecodingContainer.Key) throws -> Double {
do {
let str = try self.decode(String.self, forKey: key)
if let dbl = Double(str) {
return dbl
}
} catch DecodingError.typeMismatch {
return try self.decode(Double.self, forKey: key)
}
let context = DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Wrong Money Value")
throw DecodingError.typeMismatch(Double.self, context)
}
}
Usage:
let data = """
{"value":"1.2"}
""".data(using: .utf8)!
struct Test: Decodable {
let value: Double
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
self.value = try decoder.container(keyedBy: CodingKeys.self)
.decode(forKey: CodingKeys.value)
}
}
try JSONDecoder().decode(Test.self, from: data).value
// Out Put Json
{
"software_id": "10",
"name": "Kroll"
},
{
"software_id": 580,
"name": "Synmed"
}
// Codable Struct
struct SoftwareDataModel: Codable {
var softwareId:MyValue?
var name:String?
enum CodingKeys: String, CodingKey{
case softwareId = "software_id"
case name
}
}
MYValue is Codable Struct Which help to to convert your datatype into "String" here I mentions only String and Int datatypes.
enum MyValue: Codable {
case string(String)
var stringValue: String? {
switch self {
case .string(let s):
return s
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(Int.self) {
self = .string("\(x)")
return
}
throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
}
}
}
// How to get software_id ?
let softwareId = Struct_object.softwareId?.stringValue ?? "0"