2

I'm trying to decode JSON object where one of the properties can be either array or single object. My idea was to introduce enum with single and multiple cases. Simplified code example below:

struct Show: Codable {
  var artistBio: ArtistBios?
}

enum ArtistBios: Codable {
  case single(ArtistBio)
  case multiple([ArtistBio])
  
  init(from decoder: Decoder) throws {
    var container = try decoder.unkeyedContainer()
    if container.count != nil {
      let value = try container.decode([ArtistBio].self)
      self = .multiple(value)
    } else {
      let value = try container.decode(ArtistBio.self)
      self = .single(value)
    }
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.unkeyedContainer()
    switch self {
      case .single(let artistBio):
        try container.encode(artistBio)
      case .multiple(let artistBios):
        try container.encode(artistBios)
    }
  }
}

struct ArtistBio: Codable {
  var name: String
  var url: String
}

let jsonStrArray = """
{
  "artistBio": [
    {
      "url": "url_to_pdf1",
      "name": "Artist Name 1"
    },
    {
      "url": "url_to_pdf2",
      "name": "Artist Name 2"
    }
  ]
}
"""

let jsonStrSingle = """
{
  "artistBio": {
    "url": "url_to_pdf1",
    "name": "Artist Name"
  }
}
"""


do {
  let show = try JSONDecoder().decode(Show.self, from: jsonStrArray.data(using: .utf8)!)
  print("show", show)
} catch (let error) {
  print("decode multiple error", error)
}

do {
  let show = try JSONDecoder().decode(Show.self, from: jsonStrSingle.data(using: .utf8)!)
  print("show", show)
} catch (let error) {
  print("decode single error", error)
}

I can't figure out the way how to properly decode my enum object. In both cases for single and array version of input JSON I get this error:

typeMismatch(Swift.Array<Any>, 
Swift.DecodingError.Context(codingPath: [
CodingKeys(stringValue: "artistBio", intValue: nil), 
_JSONKey(stringValue: "Index 0", intValue: 0)], 
debugDescription: 
"Expected to decode Array<Any> but found a dictionary instead.", 
underlyingError: nil))
Matej Ukmar
  • 2,157
  • 22
  • 27
  • 1
    Use a `singleValueContainer` instead when you decode, and `try` to decode into `[ArtistBio].self` first, then if error - into `ArtistBio.self` – New Dev Dec 24 '20 at 15:51
  • Yes it works, thanks. I was a bit mislead as documentation says you can decode single `primitive` value with `singleValueContainer`. I guess objects are primitive in this case :) – Matej Ukmar Dec 24 '20 at 17:26

1 Answers1

2

You have to use a singleValueContainer, not an unkeyedContainer, the latter expects always an array.

enum ArtistBios: Codable {
  case single(ArtistBio)
  case multiple([ArtistBio])
  
  init(from decoder: Decoder) throws {
    let container = try decoder.singleValueContainer()
    do {
      let value = try container.decode([ArtistBio].self)
      self = .multiple(value)
    } catch DecodingError.typeMismatch {
      let value = try container.decode(ArtistBio.self)
      self = .single(value)
    }
  }
  
  func encode(to encoder: Encoder) throws {
    var container = encoder.singleValueContainer()
    switch self {
      case .single(let artistBio):
        try container.encode(artistBio)
      case .multiple(let artistBios):
        try container.encode(artistBios)
    }
  }
}
vadian
  • 274,689
  • 30
  • 353
  • 361