Декодирование перечисления, содержащего логические значения

У меня есть этот файл JSON.

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

Я создал структуру Month для декодирования словарей в JSON.

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

extension Month: Decodable { }

И структура Year, чтобы содержать их все.

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

Моя структура Holiday немного сложнее из-за наличия перечисления HolidayType, где я хочу декодировать значения в поле type в 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)
    }
}

Здесь я загружаю файл и декодирую.

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

Но это не удается со следующей ошибкой.

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

Я не могу понять, как это исправить. Я также загрузил демонстрационный проект здесь.

В дополнение к проблемам с перечислением, упомянутым ниже, обратите внимание, что объект верхнего уровня является массивом, поэтому вам нужно декодировать [Month].self, а не Year.self.

Martin R 22.06.2019 16:37

@MartinR У него есть собственное декодирование для Year.

Sulthan 22.06.2019 16:38

Ваша структура Year избыточна, просто расшифруйте let months = try decoder.decode([Month].self, from: data); print(months). И extension только для принятия Decodable тоже избыточны. Пустое расширение только для принятия протокола имеет смысл, если вы не являетесь владельцем структуры или класса.

vadian 22.06.2019 16:39

@Sulthan: Ты прав, я этого не заметил.

Martin R 22.06.2019 16:39

@vadian Имеет смысл обернуть месяцы в именованный тип. Я не вижу проблемы в правильном именовании типов, это не избыточность.

Sulthan 22.06.2019 16:43

@Sulthan IMHO имеет смысл, если бы было другое свойство name или number, но не обертывать одно свойство на верхнем уровне иерархии.

vadian 22.06.2019 16:47

@vadian Я постоянно создаю новые типы, просто оборачиваю String (например, AccountNumber или Currency) или Decimal (например, Amount, InterestRate). Вы не знаете, какие еще методы могут быть объявлены для данного типа, и проще передать year: Year вместо year: [Month].

Sulthan 22.06.2019 16:49

@Sulthan Я понимаю, но (на данный момент) в JSON нет ключа year...

vadian 22.06.2019 16:52

... а структура singleValueContainer происходит от этот ответ предыдущего вопроса ОП, где значение было совершенно другим.

vadian 22.06.2019 17:01
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
9
464
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

isNationalHoliday - это логическое значение, а не тип перечисления, так что для isRegionalHoliday, isPublicHoliday, isGovernmentHoliday

Тебе нужно

// 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)
Ответ принят как подходящий

Вы не можете использовать перечисление для представления нескольких логических значений. Если вы хотите сохранить свои типы без изменений, я бы рекомендовал использовать OptionSet с пользовательским декодированием:

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

или вместо пользовательского синтаксического анализа вы можете перевести свои типы с помощью вычисляемой переменной:

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

Спасибо! Использование OptionSets — это совершенно новая для меня территория. Никогда не знал, что вы можете заставить их действовать как перечисления.

Isuru 23.06.2019 05:13

@Isuru Точнее, вы можете заставить их вести себя как Set<enum>. Объявление enum HolidayType и последующее использование Set<HolidayType> для типа не имеет большого значения.

Sulthan 23.06.2019 08:36

Другие вопросы по теме