Swift: пользовательский кодировщик/декодер не может декодировать массив, вместо массива найдена строка

Из-за [AnyHashable: Any] (мне это нужно) я должен реализовать init(from:) и encode(to:). Но когда я запускаю его, ему не удается декодировать свойство со значением массива:

typeMismatch(Swift.String, Swift.DecodingError.Context(codingPath: [_JSONKey(stringValue: "items", intValue: nil)], debugDescription: "Ожидается декодирование строки, но вместо этого найден массив.", baseError: nil))

Это код, который вы можете запустить в Playground:

struct ServerResponse: Codable {
    var headers: [AnyHashable: Any]?
    var items: [Item]?
    
    enum CodingKeys: String, CodingKey {
        case items, headers
    }
    
    public init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: ServerResponse.CodingKeys.self)
        items = try container.decode([Item].self, forKey: ServerResponse.CodingKeys.items)
        let singleValueContainer = try decoder.singleValueContainer()
        let stringDictionary = try singleValueContainer.decode([String: String].self)
        headers = [:]
        for (key, value) in stringDictionary {
            headers?[key] = value
        } 
    }
    
    public func encode(to encoder: Encoder) throws {
        let stringDictionary: [String: String] = Dictionary(
            uniqueKeysWithValues: headers?.map {("\($0)", "\($1)")} ?? []
        )
        var singleValueContainer = encoder.singleValueContainer()
        try singleValueContainer.encode(stringDictionary)
        
        var container = encoder.container(keyedBy: ServerResponse.CodingKeys.self)
        try container.encode(items, forKey: ServerResponse.CodingKeys.items)
    }

    struct Item: Codable {
        let name: String
    }
}

let testData = """
    {
        "items": [
                    {"name": "John"},
                    {"name": "Duo"}
                ]
    }
    """.data(using: .utf8)!
let decoder = JSONDecoder()
do {
    let response = try decoder.decode(ServerResponse.self, from: testData)
    print(response)
} catch {
    print(error)
}

Что с этим не так? Почему он жалуется на получение String, когда я поставил массив? Если я удаляю заголовки из структуры и соответствую Codable, все работает нормально.

Ваш контейнер с одним значением обертывает { "items": [ ... ] }, и вы пытаетесь декодировать его как [String: String], но значением ключа items является массив, а не String. Можете ли вы подтвердить, что закомментирование строк после items = try container... приводит к тому, что ошибка не возникает? (Если это так, есть еще способы декодировать элементы заголовка, которые вы ищете.)

Itai Ferber 15.04.2023 01:44

Проблема не в заголовках, если данные для заголовков существуют, проблема будет работать, проблема в элементах.

Maysam 15.04.2023 01:47

@ItaiFerber Да, комментирование этих строк исправит ошибку

Maysam 15.04.2023 01:50

Не имеет отношения к этому, но почему вы конвертируете сильно типизированный [String:String] в слабо типизированный [AnyHashable: Any]?

vadian 15.04.2023 06:41

Это исходит из другого протокола, которому я должен соответствовать.

Maysam 15.04.2023 07:00
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
5
62
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Проблема здесь заключается в том, как вы пытаетесь извлечь элементы из этого словаря верхнего уровня, пытаясь декодировать его как словарь. Конкретно,

let singleValueContainer = try decoder.singleValueContainer()
let stringDictionary = try singleValueContainer.decode([String: String].self)

является проблемным фрагментом. С вашей конкретной полезной нагрузкой JSON singleValueContainer здесь завершается

{
    "items": [ ... ],
    "..." // <- assuming there are other actual keys and values
}

Это допустимо, но когда вы пытаетесь декодировать содержимое контейнера как [String: String], вы утверждаете, что ожидаете, что контейнер будет содержать конкретно словарь с ключами String и значениями String; значение для ключа items, однако, является не строкой, а массивом.

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

struct AnyCodingKey: CodingKey {
    let intValue: Int?
    let stringValue: String

    init?(intValue: Int) {
        self.intValue = intValue
        self.stringValue = "\(intValue)"
    }

    init?(stringValue: String) {
        intValue = Int(stringValue)
        self.stringValue = stringValue
    }
}

С помощью этого типа ключа кодирования вы можете запросить decoder в качестве другого контейнера с ключом — и на этот раз ключи могут быть произвольными:

let untypedContainer = try decoder.container(keyedBy: AnyCodingKey.self)

Хитрость в том, что теперь значения в untypedContainer не считаются принадлежащими какому-либо типу, пока вы не попытаетесь их декодировать. Затем вы можете перебирать untypedContainer.allKeys (так же, как вы перебираете stringDictionary в данный момент), и для каждого ключа вы можете решить, как вы хотите decode(_:forKey:) выйти из контейнера. Вы могли бы:

  1. Попытайтесь расшифровать String, и если вы получите ошибку DecodinerError.typeMismatch, просто пропустите пару ключ-значение
  2. Проверьте значение каждого ключа, и если у вас есть известные ключи (или ключи, которые соответствуют какому-то шаблону или тому подобное), которые вы ищете, вместо этого декодируйте только те

Например:

public init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: ServerResponse.CodingKeys.self)
    items = try container.decode([Item].self, forKey: ServerResponse.CodingKeys.items)

    headers = [:]
    let untypedContainer = try decoder.container(keyedBy: AnyCodingKey.self)
    for key in untypedContainer.allKeys {
        do {
            let value = try untypedContainer.decode(String.self, forKey: key)
            headers[key.stringValue] = value
        } catch DecodingError.typeMismatch { /* skip this key-value pair */ }
    }
}

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