解码没有字段名称的 JSON 数组

Decoding a JSON array that has no field name

我有一个像这样的简单 JSON 文件。

{
    "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
}

我正在尝试使用 Swift 的 JSONDecoder 将它们解码为对象。为此,我创建了一个 Month 和一个 Holiday 对象。

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 { }

还有一个单独的 HolidayData 模型来保存所有这些数据。

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

extension HolidayData: Decodable { }

这是我进行解码的地方。

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
}

但它一直失败并出现以下错误。

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

我猜它失败了,因为 JSON 文件中没有名为 holidays 的字段,尽管 Month 结构中有一个。

如何将假期数组添加到 holidays 字段中,而不是将其包含在 JSON 中?

你的JSON结构很难解码,但可以做到。

这里的关键是你需要一个像这样的 CodingKey 枚举(双关语):

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

并且您可以在 HolidayData 结构中提供 init(decoder:) 的自定义实现:

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
    }
}

另请注意,结构的 属性 名称与 JSON 中的键名称不同。打字错误?

月份结构与 json 不匹配。

将月份结构更改为类似这样的其他内容:

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 { }

请注意,这不是您如何实现所需目标的最佳实践。

另一种方法是改变json(如果你有权限)以匹配你的结构:

{[
    "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
    ]
}

如果想在不编写自定义解码逻辑的情况下解析JSON,可以按如下方式进行:

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)

为此我不得不制作 isBankHolidayisMercantileHoliday Optional 因为它们并不总是出现在 JSON.


现在,如果你想将它解码成你上面介绍的结构,你将不得不编写自定义解码逻辑:

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)