0

I have a simple JSON file like this.

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

I'm trying to use Swift's JSONDecoder to decode these into objects. For that, I have created a Month and a Holiday object.

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

extension Month: Decodable { }

public struct Holiday {
    public let name: String
    public let date: Date
    public let isNationalHoliday: Bool
    public let isRegionalHoliday: Bool
    public let isPublicHoliday: Bool
    public let isGovernmentHoliday: Bool
}

extension Holiday: Decodable { }

And a separate HolidayData model to hold all those data.

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

extension HolidayData: Decodable { }

This is where I'm doing the decoding.

guard let url = Bundle.main.url(forResource: "holidays", withExtension: "json") else { return }
do {
    let data = try Data(contentsOf: url)
    let decoder = JSONDecoder()
    decoder.dateDecodingStrategy = .iso8601
    let jsonData = try decoder.decode(Month.self, from: data)
    print(jsonData)
} catch let error {
    print("Error occurred loading file: \(error.localizedDescription)")
    return
}

But it keeps failing with the following error.

The data couldn’t be read because it isn’t in the correct format.

I'm guessing it's failing because there is no field called holidays in the JSON file even though there is one in the Month struct.

How do I add the holidays array into the holidays field without having it in the JSON?

Sweeper
  • 213,210
  • 22
  • 193
  • 313
Isuru
  • 30,617
  • 60
  • 187
  • 303

3 Answers3

1

Your JSON structure is quite awkward to be decoded, but it can be done.

The key thing here is that you need a CodingKey enum like this (pun intended):

enum Months : CodingKey, CaseIterable {
    case january
    case feburary
    case march
    // ...
}

And you can provide a custom implementation of init(decoder:) in your HolidayData struct:

extension HolidayData : Decodable {
    public init(from decoder: Decoder) throws {
        var months = [Month]()
        let container = try decoder.container(keyedBy: Months.self)
        for month in Months.allCases {
            let holidays = try container.decodeIfPresent([Holiday].self, forKey: month)
            months.append(Month(name: month.stringValue, holidays: holidays))
        }
        self.months = months
    }
}

Also note that your structs' property names have different names from the key names in your JSON. Typo?

Sweeper
  • 213,210
  • 22
  • 193
  • 313
  • Thanks for the response :) I see. Someone else told me about the problem with the JSON structure too, I can have it changed. How about something like [this](https://pastebin.com/raw/FAe3xZdK)? – Isuru Jun 22 '19 at 09:10
  • @Isuru That's much better! – Sweeper Jun 22 '19 at 09:11
  • Gotcha! I'll change the JSON structure and give it a one more go. – Isuru Jun 22 '19 at 09:13
  • Okay, I changed the JSON structure to that and replaced the `HolidayData` struct with a `Year` struct. It only has one property called `months` of `[Month]` type. Now I have to manually decode the month dictionaries in the `init` method of the `Year` struct, right? But my question is how do I define a `CodingKey` for the months array when there is no field called `months` in the JSON? – Isuru Jun 22 '19 at 09:34
  • @Isuru I would declare `HolidayData` as a type alias of `[Month]`, and `Month` would have a `month: String` and a `holidays: [Holiday]` properties. – Sweeper Jun 22 '19 at 09:43
  • I'll look into it. Thanks. I updated my question to include all the new details btw. – Isuru Jun 22 '19 at 09:51
  • @Isuru Your update to the question kind of invalidates a lot of the existing answers here... Could you please rollback your edit and post a new question? It would also be nice if you could accept an answer here. – Sweeper Jun 22 '19 at 09:54
  • I see. Is there a way to see my previous edits here? I can't seem to find it. – Isuru Jun 22 '19 at 09:56
1

If you want to parse the JSON without writing custom decoding logic, you can do it as follows:

public struct Holiday: Decodable {
    public let name: String
    public let date: Date
    public let isBankHoliday: Bool?
    public let isPublicHoliday: Bool
    public let isMercantileHoliday: Bool?
}

try decoder.decode([String: [Holiday]?].self, from: data)

For that I had to make isBankHoliday and isMercantileHoliday Optional as they don't always appear in the JSON.


Now, if you want to decode it into the stucture that you introduced above, you'll have to write custom decoding logic:

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

extension Month: Decodable { }

public struct Holiday {
    public let name: String
    public let date: Date
    public let isBankHoliday: Bool
    public let isPublicHoliday: Bool
    public let isMercantileHoliday: Bool

    enum CodingKeys: String, CodingKey {
        case name
        case date
        case isBankHoliday
        case isPublicHoliday
        case isMercantileHoliday
    }
}

extension Holiday: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        name = try container.decode(String.self, forKey: .name)
        date = try container.decode(Date.self, forKey: .date)
        isBankHoliday = try container.decodeIfPresent(Bool.self, forKey: .isBankHoliday) ?? false
        isPublicHoliday = try container.decodeIfPresent(Bool.self, forKey: .isPublicHoliday) ?? false
        isMercantileHoliday = try container.decodeIfPresent(Bool.self, forKey: .isMercantileHoliday) ?? false
    }
}

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

extension HolidayData: Decodable {
    public init(from decoder: Decoder) throws {
        let container = try decoder.singleValueContainer()
        let values = try container.decode([String: [Holiday]?].self)

        months = values.map { (name, holidays) in
            Month(name: name, holidays: holidays)
        }
    }
}

decoder.decode(HolidayData.self, from: data)
fphilipe
  • 9,739
  • 1
  • 40
  • 52
  • Thanks for the detailed answer :) However I'm getting a new error _The data couldn’t be read because it is missing_. I uploaded a demo project [here](https://www.dropbox.com/s/bx2gnahzvddskdh/JSONDecoding.zip?dl=0) if you could please take a look. – Isuru Jun 22 '19 at 10:16
  • 1
    @Isuru **Never** `print(error.localizedDescription)` in a `JSONDecoder` catch block. Always `print(error)`. It tells you exactly what's wrong. – vadian Jun 22 '19 at 10:31
  • @vadian Got it! Fixed the issue. Thanks a ton to both of you. – Isuru Jun 22 '19 at 10:37
0

Month structure does not match with the json.

Change month structure to something else like this:

public struct Year {
     public let January: [Holyday]?
     public let February: [Holyday]?
     public let March: [Holyday]?
     public let April: [Holyday]?
     public let May: [Holyday]?
     public let June: [Holyday]?
     public let July: [Holyday]?
     public let August: [Holyday]?
     public let September: [Holyday]?
     public let October: [Holyday]?
     public let November: [Holyday]?
     public let December: [Holyday]?
}

extension Year: Decodable { }

Note that it is not a best practice of how you can achieve what you want.

Another way is to change the json (if you have access) to match you structures:

{[
    "name":"january",
    "holidays": [
        {
            "name": "New Year's Day",
            "date": "2019-01-01T00:00:00-0500",
            "isNationalHoliday": true,
            "isRegionalHoliday": true,
            "isPublicHoliday": true,
            "isGovernmentHoliday": true
        },
        {
            "name": "Martin Luther King Day",
            "date": "2019-01-21T00:00:00-0500",
            "isNationalHoliday": true,
            "isRegionalHoliday": true,
            "isPublicHoliday": true,
            "isGovernmentHoliday": true
        }
    ]],[
    "name":"february",
    "holidays": [
        {
            "name": "Presidents' Day",
            "date": "2019-02-18T00:00:00-0500",
            "isNationalHoliday": false,
            "isRegionalHoliday": true,
            "isPublicHoliday": false,
            "isGovernmentHoliday": false
        }
    ]],[
    "name":"march",
    "holidays": null
    ]
}
Mojtaba Hosseini
  • 95,414
  • 31
  • 268
  • 278
  • I actually changed my JSON to look something exactly like that (`month` field instead of `name`). Now I'm having a new issue. I updated my original question above. – Isuru Jun 22 '19 at 09:46