Home > OS >  Swift Decodable with inconsistent API
Swift Decodable with inconsistent API

Time:11-11

I was wondering if there is a way to handle decoding JSON into a struct, when the API sending the JSON is potentially inconsistent with it's typing. In this case, it sometimes sends a property as an array, and other times as a string. I am not sure how to handle that, or if there is a nice way with Decodable. Example below. My Struct:

struct Movie: Decodable {
    let title: String
    let cast: [String]
    let director: [String]
}

The JSON:

[{
  "title": "Pulp Fiction",
  "cast": [
    "John Travolta",
    "Uma Thurman",
    "Samuel L. Jackson"
  ],
  "director": "Quentin Tarantino"
},
{
  "title": "Spider-Man: Into the Spider-Verse",
  "cast": [
    "John Travolta",
    "Uma Thurman",
    "Samuel L. Jackson"
  ],
  "director": [
    "Bob Persichetti",
    "Peter Ramsey",
    "Rodney Rothman"
  ]
}]

I am able to decode Spider-Man without an issue, but if there is only one director, it comes through as a string instead of an array. Is there a way to use Decodable for this? I know I could manually build the structs, but it would be nice to not have to. Unfortunately I have no control over the API here. Thanks in advance.

CodePudding user response:

The most versatile way to decode different types is an enum with associated values.

But in this case where the types are quite related you can also write a custom initializer, the struct member is named in plural form and is declared as array

struct Movie: Decodable {
    let title: String
    let cast: [String]
    let directors: [String]
    
    private enum CodingKeys : String, CodingKey { case title, cast, director }
    
    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self.title = try container.decode(String.self, forKey: .title)
        self.cast = try container.decode([String].self, forKey: .cast)
        if let singleDirector = try? container.decode(String.self, forKey: .director) {
            self.directors = [singleDirector]
        } else {
            self.directors = try container.decode([String].self, forKey: .director)
        }
    }
}

CodePudding user response:

struct Movie: Codable {
    let title: String
    let cast: [String]
    let director: Director
}

enum Director: Codable {
    case string(String)
    case stringArray([String])

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode([String].self) {
            self = .stringArray(x)
            return
        }
        
        if let x = try? container.decode(String.self) {
            self = .string(x)
            return
        }
        throw DecodingError.typeMismatch(Director.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for Director"))
    }

    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .string(let x):
            try container.encode(x)
        case .stringArray(let x):
            try container.encode(x)
        }
    }
}

typealias Movies = [Movie]
  • Related