5

The services I use for backend calls returns all this json-structure:

{
    "status" : "OK",
    "payload" : **something**
}

Where something can be a simple string:

{
    "status" : "OK",
    "payload" : "nothing changed"
}

or a nested json (any json with any properties), for example:

{
    "status" : "OK",
    "payload" : {
                    "someInt" : 2,
                    "someString" : "hi",
                    ...
                }
}

This is my struct:

struct GenericResponseModel: Codable {

    let status:String?
    let payload:String?
}

I want to decode "payload" always as a string. So in the second case I want that the payload property of my "GenericResponseModel" contains the json string of that field, but If I try to decode that response I get the error:

Type 'String' mismatch: Expected to decode String but found a dictionary instead

Is possible to archive what I want?

Many thanks

smukamuka
  • 1,442
  • 1
  • 15
  • 23
  • Yes, this is possible by specifying how your `GenericResponseModel ` should decode itself. Check `init(from decoder: Decoder)`. – Sander Saelmans Oct 17 '18 at 09:56
  • https://stackoverflow.com/a/50674899/6630644 – SPatel Oct 17 '18 at 09:58
  • This service returns a JSON with a key that might have different types? – JBL Oct 17 '18 at 10:25
  • @JBL: yes, it can return different object types, based on the service endpoint – smukamuka Oct 17 '18 at 10:26
  • In that case it looks like you'll need to decode `payload` as a string first, and if it fails, as an arbitrary JSON, and [this might help](https://stackoverflow.com/q/44603248/1594913). Then serialize it (e.g. through JSONSerialization to Data to String with [`init?(data:encoding:)`](https://developer.apple.com/documentation/swift/string/1418413-init) ) – JBL Oct 17 '18 at 10:29
  • I'd suggest you speak to your backend team and suggest they improve their naming - e.g. `payload` when it's JSON, and `message` for a `String`. Or use the `someString` field in `Payload`, and make `someInt` optional – Ashley Mills Oct 17 '18 at 10:51
  • the fact that this has only gotten a few dozen upvotes and no reasonable answer in 1.5 years makes me wonder what is going on in the swift community :P – roberto tomás Apr 26 '19 at 01:40

1 Answers1

4

How about this…

Declare a PayloadType protocol…

protocol PayloadType: Decodable { }

and make String, and struct Payload conform to it…

extension String: PayloadType { }

struct Payload: Decodable, PayloadType {
    let someInt: Int
    let someString: String
}

Then make GenericResponseModel generic…

struct GenericResponseModel<T: PayloadType>: Decodable {

    let status: String
    let payload: T

    enum CodingKeys: CodingKey {
        case status, payload
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        status = try container.decode(String.self, forKey: .status)            
        payload = try container.decode(T.self, forKey: .payload)
    }
}

Then you can decode as follows…

let data = """
{
"status" : "OK",
"payload" : "nothing changed"
}
""".data(using: .utf8)!

print(try JSONDecoder().decode(GenericResponseModel<String>.self, from: data))

// GenericResponseModel<String>(status: "OK", payload: "nothing changed")

and

let data2 = """
{
"status" : "OK",
"payload" : {
"someInt" : 2,
"someString" : "hi"
}
}
""".data(using: .utf8)!

print(try JSONDecoder().decode(GenericResponseModel<Payload>.self, from: data2))

// GenericResponseModel<Payload>(status: "OK", payload: Payload(someInt: 2, someString: "hi"))


Of course, this relies on you knowing the payload type in advance. You could get around this by throwing a specific error if payload is the wrong type…

enum GenericResponseModelError: Error {
    case wrongPayloadType
}

and then…

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    status = try container.decode(String.self, forKey: .status)

    do {
        payload = try container.decode(T.self, forKey: .payload)
    } catch {
        throw GenericResponseModelError.wrongPayloadType
    }
}

Then handle this error when you decode…

let data = """
{
"status" : "OK",
"payload" : {
"someInt" : 2,
"someString" : "hi"
}
}
""".data(using: .utf8)!

do {
    let response = try JSONDecoder().decode(GenericResponseModel<String>.self, from: data) // Throws
    print(response) 
} catch let error as GenericResponseModelError where error == .wrongPayloadType {
    let response = try JSONDecoder().decode(GenericResponseModel<Payload>.self, from: data2) // Success!
    print(response)
}
Ashley Mills
  • 50,474
  • 16
  • 129
  • 160
  • Also in this case, It won't work on all kind of json-object contained in "payload" but only with the example I made. Payload can be every kind of object. – smukamuka Oct 17 '18 at 12:32
  • 1
    How can we be expected to answer the question when you don't give all the relevant information? Also, I don't believe _"Payload can be every kind of object."_ - if it can be literally _anything_, then `Codable` is the wrong solution. Use `JSONSerialization` and create a Dictionary from your JSON. Do you actually mean _"The payload varies for each request"_? – Ashley Mills Oct 17 '18 at 12:46
  • In the question is written "any json with any properties", so all informations has been given. In fact, my question is how to treat the param as a string, regardless its real type, and I just asked if this goal could be reached. – smukamuka Oct 17 '18 at 12:55