问题
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?
回答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)
回答2:
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?
回答3:
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
]
}
来源:https://stackoverflow.com/questions/56713939/decoding-a-json-array-that-has-no-field-name