3

I have a struct named Info which is decoded based on the data it receives. But sometimes, one of the values in data can either be a double or an array of double. How do I set up my struct for that?

struct Info: Decodable {
    let author: String
    let title: String
    let tags: [Tags]
    let price: [Double]
    enum Tags: String, Decodable {
        case nonfiction
        case biography
        case fiction
    }
}

Based on the url, I either get price as a double

{
    "author" : "Mark A",
    "title" : "The Great Deman",
    "tags" : [
      "nonfiction",
      "biography"
    ],
    "price" : "242"

}

or I get it as an array of doubles

{
    "author" : "Mark A",
    "title" : "The Great Deman",
    "tags" : [
      "nonfiction",
      "biography"
    ],
    "price" : [
    "242",
    "299",
    "335"
    ]

}

I want to setup my struct so that if I receive a double instead of an array of doubles, price should be decoded as an array of 1 double.

Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
Singh Raman
  • 103
  • 1
  • 5

2 Answers2

7

Your JSON actually is either a String or an array of Strings. So you need to create a custom decoder to decode and then convert them to Double:

struct Info {
    let author, title: String
    let tags: [Tags]
    let price: [Double]
    enum Tags: String, Codable {
        case nonfiction, biography, fiction
    }
}

extension Info: Codable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        author = try container.decode(String.self, forKey: .author)
        title  = try container.decode(String.self, forKey: .title)
        tags = try container.decode([Tags].self, forKey: .tags)
        do {
            price = try [Double(container.decode(String.self, forKey: .price)) ?? .zero]
        } catch {
            price = try container.decode([String].self, forKey: .price).compactMap(Double.init)
        }
    }
}

Playground testing

let infoData = Data("""
{
    "author" : "Mark A",
    "title" : "The Great Deman",
    "tags" : [
      "nonfiction",
      "biography"
    ],
    "price" : "242"

}
""".utf8)
do {
    let info = try JSONDecoder().decode(Info.self, from: infoData)
    print("price",info.price)  // "price [242.0]\n"
} catch {
    print(error)
}

let infoData2 = Data("""
{
    "author" : "Mark A",
    "title" : "The Great Deman",
    "tags" : [
      "nonfiction",
      "biography"
    ],
    "price" : [
    "242",
    "299",
    "335"
    ]

}
""".utf8)

do {
    let info = try JSONDecoder().decode(Info.self, from: infoData2)
    print("price",info.price)  // "price [242.0, 299.0, 335.0]\n"
} catch {
    print(error)
}
Leo Dabus
  • 229,809
  • 59
  • 489
  • 571
0

Let me suggest a generic solution for cases where we can deal with one or several values in Codable.

import Foundation

// Equatable confarmance is for checking correct encoding/decoding operations only
enum OneOrMany<U: Codable>: Codable, Equatable where U: Equatable {
    case one(U)
    case many([U])
    
    // Decodable conformance
    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        if let x = try? container.decode(U.self) {
            self = .one(x)
            return
        }
        if let x = try? container.decode([U].self) {
            self = .many(x)
            return
        }
        throw DecodingError.typeMismatch(OneOrMany.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Unexpected assosiated type for an enum"))
    }
    // Encodable conformance
    func encode(to encoder: Encoder) throws {
        var container = encoder.singleValueContainer()
        switch self {
        case .one(let x):
            try container.encode(x)
        case .many(let x):
            try container.encode(x)
        }
    }
}

Here is a way to test it in playground:

struct Info: Codable, Equatable {
    let author: String
    let title: String
    let tags: [Tags]
    let price: OneOrMany<String>
    enum Tags: String, Codable {
        case nonfiction
        case biography
        case fiction
    }
}

let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let decoder = JSONDecoder()

let onePriceData = """
{
"author" : "Mark A",
"title" : "The Great Deman",
"tags" : [
    "nonfiction",
    "biography"
],
"price" : "242"

}
""".data(using: .utf8)!

let severalPricesData = """
{
    "author" : "Mark A",
    "title" : "The Great Deman",
    "tags" : [
      "nonfiction",
      "biography"
    ],
    "price" : [
    "242",
    "299",
    "335"
    ]

}
""".data(using: .utf8)!

let onePrice = try decoder.decode(Info.self, from: onePriceData)
dump(onePrice)

let onePriceDataEncoded = try encoder.encode(onePrice)
print(String(data: onePriceDataEncoded, encoding: .utf8)!)

let onePrice2 = try decoder.decode(Info.self, from: onePriceDataEncoded)

let severalPrices = try decoder.decode(Info.self, from: severalPricesData)

let severalPricesDataEncoded = try encoder.encode(severalPrices)

let severalPrices2 = try decoder.decode(Info.self, from: severalPricesDataEncoded)


import XCTest

class JSONEncodeDecodeTestCase : XCTestCase {
    
    func testOnePriceDecodedEncodedSuccessfully() {
        XCTAssertEqual(onePrice, onePrice2)
    }
    
    func testSeveralPricesDecodedEncodedSuccessfully() {
        XCTAssertEqual(severalPrices, severalPrices2)
    }
}

JSONEncodeDecodeTestCase.defaultTestSuite.run()

Side note: here we can also make use of StringBacked<Value: StringRepresentable>: Codable for transforming values, because Double values are encoded as strings for some reason in provided JSON.

Paul B
  • 3,989
  • 33
  • 46