0

I have the following struct, and try to decode the object as follows.

I am using Swift 4.3.

struct Classroom: Codable {
  let teacher: Teacher
  let id: Int
  let status: String?
}

let classes: Classroom = try clasroom.decodeObject()

However, status object has two different types either String or Dictionary. If it comes as a Dictionary, then I am only interested to assign the key to the status.

it works in the following case,

{"teacher": {"name": "Carolina"},"id": 20,"status": "Success"}

Wondering how to handle these type of scenarios?

{"id": 20, "teacher": {"name": "Carolina"},"status":{"Failure":"network is down"}}
casillas
  • 16,351
  • 19
  • 115
  • 215
  • make "status" a generic object not string – zeytin Jan 27 '21 at 20:04
  • Declare `status` als enum with associated types. @impression7vx You cannot decode `Any`. – vadian Jan 27 '21 at 20:05
  • 2
    You'd need to manually implement `init(from decoder: Decoder) throws` and try decoding as `String`, then if that fails, try decoding as the other type, and getting the property you want. See if this helps: https://stackoverflow.com/a/64307098/968155 – New Dev Jan 27 '21 at 20:06
  • @vadian, I have added my enum and updated my question, wondering how could I able to use it? – casillas Jan 27 '21 at 20:07
  • I suppose not @vadian. I was considering the notion that he could break upon that; but that is more work than necessary – impression7vx Jan 27 '21 at 20:08
  • 3
    @casillas You have to implement `init(from decoder ; Decoder`. – vadian Jan 27 '21 at 20:09
  • @vadian, would you mind to illustrate with an example? – casillas Jan 27 '21 at 20:11
  • Are you missing a `"teacher": ` in your second (last) example JSON? – Rob Napier Jan 27 '21 at 20:12
  • @RobNapier sorry , yes I have corrected – casillas Jan 27 '21 at 20:13
  • @casillas you are changing the question when i answer. please ask the one with correct format at first of all – zeytin Jan 27 '21 at 20:13
  • @zeytin, only updated missing `"teacher":`. The question is final. – casillas Jan 27 '21 at 20:15
  • 1
    It's unclear but if you are only interested on status Succes or failure (without the reason) AND the status is a String when success and a Dictionary with reason when failure, then you can simply have a check "on the type" in your custom `init(from: decoder)`: `self.status = try? container.decode(String.self, forKey: .status) ?? "Failure"`. – Larme Jan 27 '21 at 20:19

2 Answers2

3

Declare status as enum with associated values

enum Status : Decodable {
    
    case success, failure(String)
    
    init(from decoder : Decoder) throws
    {
        let container = try decoder.singleValueContainer()
        do {
            try container.decode(String.self)
            self = .success
        } catch {
            let error = try container.decode(StatusError.self)
            self = .failure(error.Failure)
        }
    }
}

and a helper struct

struct StatusError : Decodable {
    let Failure : String
}

In Classroom declare

let status: Status

And check the status

switch classroom.status {
    case .success: print("OK")
    case .failure(let message): print(message)
} 

Of course the error handling can be more robust: Is the success string really "Success"? And you can decode the failure type as [String:String] and get the value for key Failure.

vadian
  • 274,689
  • 30
  • 353
  • 361
  • @vadian, if I have multiple cases such as `.success`, `.accepted`, `.rejected(String)`, `.failure(String)`, etc.. How do we handle that? – casillas Jan 27 '21 at 20:33
  • I call this sort of enum, rightly or wrongly, a "union" — it is a type that can represent any of several different types. I think other people have other names for it but I forget what they are. – matt Jan 27 '21 at 20:34
  • But in your solution, you assume that if it is just `String` and take it as `.success`, if not then take it as `.failure` – casillas Jan 27 '21 at 20:35
  • According to the given JSON my solution decodes `status` as `String` or as `Dictionary` (aka a struct) – vadian Jan 27 '21 at 20:37
1

You'll need a custom decoder for this. Most of it is standard, the only part that's special is the decoding of status:

extension Classroom: Decodable {
    enum CodingKeys: CodingKey {
        case teacher, id, status
    }
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.teacher = try container.decode(Teacher.self, forKey: .teacher)
        self.id = try container.decode(Int.self, forKey: .id)

        // Either decode a string, or take the first key of a [String: String]
        self.status = try
            (try? container.decode(String.self, forKey: .status)) ??
            container.decode([String: String].self, forKey: .status).keys.first
    }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • thanks a lot for proving great answer. Quick questions, where should I apply `.convertFromSnakeCaseWithDateDecoder`? and also if I just only take the status item as a new enum like Status and apply the custom decoding, would not be better? – casillas Jan 27 '21 at 20:22
  • 1
    The snake case part wouldn't change. That's unrelated to Classroom; it's entirely part of the decoder you pass in. You can certainly decode the entire Status (just replace `[String: String]` with `Status.self`). You just indicated you "only wanted the key" which isn't the same thing as verifying that the Status object is well-formed. "Better" completely depends on how you want to behave if the server sends you something slightly unexpected. The more you make mandatory, the more likely the entire decode fails. – Rob Napier Jan 27 '21 at 20:25
  • 1
    Worth emphasizing, perhaps, that if you take this route you are left with the unfortunate consequence that you have to decode the whole thing manually. There is no "partial" `init(from:)`. That is why I prefer the use an enum that self-decodes; it confines the custom part to the problematic part. :) – matt Jan 27 '21 at 20:33
  • matt makes a good point here; what he's describing matches vadian's approach. But then `status` can't be `String`; it has to be it's own type (struct or enum or whatever) – Rob Napier Jan 27 '21 at 20:37