Decoding a JSON array that has no field name

2019-08-27 00:10发布

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?

3条回答
Lonely孤独者°
2楼-- · 2019-08-27 00:20

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
    ]
}
查看更多
狗以群分
3楼-- · 2019-08-27 00:22

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)
查看更多
forever°为你锁心
4楼-- · 2019-08-27 00:44

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?

查看更多
登录 后发表回答