1

I have this JSON file.

[
    {
        "name": "January",
        "holidays": [
            {
                "name": "New Year's Day",
                "date": "2019-01-01T00:00:00-0500",
                "type": {
                    "isNationalHoliday": true,
                    "isRegionalHoliday": true,
                    "isPublicHoliday": true,
                    "isGovernmentHoliday": true
                }
            },
            {
                "name": "Martin Luther King Day",
                "date": "2019-01-21T00:00:00-0500",
                "type": {
                    "isNationalHoliday": true,
                    "isRegionalHoliday": true,
                    "isPublicHoliday": true,
                    "isGovernmentHoliday": true
                }
            }
        ]
    },
    {
        "name": "February",
        "holidays": [
            {
                "name": "Presidents' Day",
                "date": "2019-02-18T00:00:00-0500",
                "type": {
                    "isNationalHoliday": false,
                    "isRegionalHoliday": true,
                    "isPublicHoliday": false,
                    "isGovernmentHoliday": false
                }
            }
        ]
    },
    {
        "name": "March",
        "holidays": null
    }
]

I created a Month struct to decode the dictionaries in the JSON.

public struct Month {
    public let name: String
    public let holidays: [Holiday]?
}

extension Month: Decodable { }

And an Year struct to contain them all.

public struct Year {
    public let months: [Month]
}

extension Year: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let values = try container.decode([Month].self)

        months = values
    }
}

My Holiday struct is a little more complex due to having an enum called HolidayType where I want to decode the values under the type field in the JSON.

public struct Holiday {
    public let name: String
    public let date: Date
    public let type: HolidayType
}

extension Holiday: Decodable { }

public enum HolidayType {
    case isNationalHoliday
    case isRegionalHoliday
    case isPublicHoliday
    case isGovernmentHoliday

    enum CodingKeys: String, CodingKey {
        case isNationalHoliday
        case isRegionalHoliday
        case isPublicHoliday
        case isGovernmentHoliday
    }
}

extension HolidayType: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self = try container.decode(HolidayType.self, forKey: .isNationalHoliday)
        self = try container.decode(HolidayType.self, forKey: .isRegionalHoliday)
        self = try container.decode(HolidayType.self, forKey: .isPublicHoliday)
        self = try container.decode(HolidayType.self, forKey: .isGovernmentHoliday)
    }
}

This is where I'm loading the file and decoding.

if let url = Bundle.main.url(forResource: "holidays", withExtension: "json") {
    do {
        let data = try Data(contentsOf: url)
        let decoder = JSONDecoder()
        decoder.dateDecodingStrategy = .iso8601
        let year = try decoder.decode(Year.self, from: data)
        print(year.months)
    } catch let error {
        print("Error occurred decoding JSON: \(error)")
    }
} else {
    print("Error occurred loading file")
}

But it fails with the following error.

typeMismatch(Swift.Dictionary, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "holidays", intValue: nil), _JSONKey(stringValue: "Index 0", intValue: 0), CodingKeys(stringValue: "type", intValue: nil), CodingKeys(stringValue: "isNationalHoliday", intValue: nil)], debugDescription: "Expected to decode Dictionary but found a number instead.", underlyingError: nil))

I can't figure out how to fix this. I also uploaded a demo project here.

Isuru
  • 30,617
  • 60
  • 187
  • 303
  • In addition to the enum problems mentioned below, note that the top-level object is an array, so you have to decode `[Month].self` and not `Year.self`. – Martin R Jun 22 '19 at 14:37
  • @MartinR He has custom decoding for `Year`. – Sulthan Jun 22 '19 at 14:38
  • Your `Year` struct is redundant, just decode `let months = try decoder.decode([Month].self, from: data); print(months)`. And the `extension`s only to adopt `Decodable` are redundant, too. An empty extension only to adopt a protocol make sense if you are not the owner of the struct or class – vadian Jun 22 '19 at 14:39
  • @Sulthan: You are right, I did not notice that. – Martin R Jun 22 '19 at 14:39
  • @vadian It makes sense to wrap months into a named type. I don't see a problem with correctly naming types, that's not redundancy. – Sulthan Jun 22 '19 at 14:43
  • 1
    @Sulthan IMHO it makes sense if there was another property `name` or `number` but not to wrap a single property on the top level of the hierarchy. – vadian Jun 22 '19 at 14:47
  • @vadian I consistently create new types just do wrap a `String` (eg. `AccountNumber` or `Currency`) or a `Decimal` (e.g. `Amount`, `InterestRate`). You don't know what other methods can be declared on the given type and it's easier to pass `year: Year` instead of `year: [Month]`. – Sulthan Jun 22 '19 at 14:49
  • @Sulthan I understand but (at the moment) there is no `year` key in the JSON... – vadian Jun 22 '19 at 14:52
  • ... and the `singleValueContainer` struct comes from [this answer](https://stackoverflow.com/questions/56713939/decoding-a-json-array-that-has-no-field-name/56714196#56714196) of the OP's previous question where the meaning was completely different. – vadian Jun 22 '19 at 15:01

2 Answers2

3

You cannot use an enum to represent multiple booleans. If you want to keep your types without change, I would recommend using an OptionSet with custom decoding:

struct Year: Decodable {
    let months: [Month]

    init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        months = try container.decode([Month].self)
    }
}

struct Month: Decodable {
    let name: String
    let holidays: [Holiday]?
}

struct Holiday: Decodable {
    let name: String
    let date: Date
    let type: HolidayType
}

struct HolidayType: OptionSet, Decodable {
    let rawValue: Int

    static let national = HolidayType(rawValue: 1 << 0)
    static let regional = HolidayType(rawValue: 1 << 1)
    static let `public` = HolidayType(rawValue: 1 << 2)
    static let government = HolidayType(rawValue: 1 << 3)

    init(rawValue: Int) {
        self.rawValue = rawValue
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        self = try CodingKeys.allCases
            .filter { try container.decode(Bool.self, forKey: $0) }
            .map { $0.type }
            .reduce([] as HolidayType) { $0.union($1) }
    }

    private enum CodingKeys: String, CodingKey, CaseIterable {
        case isNationalHoliday
        case isRegionalHoliday
        case isPublicHoliday
        case isGovernmentHoliday

        var type: HolidayType {
            switch self {
            case .isNationalHoliday:
                return .national
            case .isRegionalHoliday:
                return .regional
            case .isPublicHoliday:
                return .public
            case .isGovernmentHoliday:
                return .government
            }
        }
    }
}

or, instead of custom parsing, you can translate your types using a computed variable:

struct Holiday: Decodable {
    let name: String
    let date: Date
    private let type: HolidayTypeHolder
    var types: [HolidayType] {
        return type.types
    }
}

enum HolidayType: String {
    case national, regional, `public`, `government`
}

private struct HolidayTypeHolder: Decodable {
    let isNationalHoliday, isRegionalHoliday, isPublicHoliday, isGovernmentHoliday: Bool

    var types: [HolidayType] {
        var types: [HolidayType] = []
        if isNationalHoliday {
            types.append(.national)
        }
        if isRegionalHoliday {
            types.append(.regional)
        }
        if isPublicHoliday {
            types.append(.public)
        }
        if isGovernmentHoliday {
            types.append(.government)
        }

        return types
    }
}
Sulthan
  • 128,090
  • 22
  • 218
  • 270
  • Thank you! Using `OptionSet`s is a completely new territory for me. Never knew you could make them act like enums. – Isuru Jun 23 '19 at 03:13
  • @Isuru More preciselly, you can make them act like `Set`. It wouldn't make much difference declaring enum `HolidayType` and then using `Set` for the type. – Sulthan Jun 23 '19 at 06:36
-1

"isNationalHoliday", intValue: nil)], debugDescription: "Expected to decode Dictionary but found a number instead.", underlyingError: nil))

isNationalHoliday is a bool value not an enum type , so on for isRegionalHoliday, isPublicHoliday, isGovernmentHoliday

You need

// MARK: - Element
struct Root: Codable {
    let name: String
    let holidays: [Holiday]?
}

// MARK: - Holiday
struct Holiday: Codable {
    let name: String
    let date: Date
    let type: TypeClass
}

// MARK: - TypeClass
struct TypeClass: Codable {
    let isNationalHoliday, isRegionalHoliday, isPublicHoliday, isGovernmentHoliday: Bool
}

let year = try decoder.decode([Root].self, from: data)
Shehata Gamal
  • 98,760
  • 8
  • 65
  • 87