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.

1
  • You mean string or array of strings check the duplicate Commented Oct 12, 2019 at 23:59

2 Answers 2

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)
}
Sign up to request clarification or add additional context in comments.

1 Comment

Wow. Thank you so much Leo. The solution worked. Much much appreciated.
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.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.