JSONEncoder не позволит закодировать тип в примитивное значение

Я работаю над реализацией Codable для типа enum с возможными связанными значениями. Поскольку они уникальны для каждого случая, я подумал, что смогу обойтись без вывода их во время кодирования, а затем просто посмотреть, что я могу получить при декодировании, чтобы восстановить правильный регистр.

Вот очень урезанный, надуманный пример, демонстрирующий своего рода динамически типизированное значение:

enum MyValueError : Error { case invalidEncoding }

enum MyValue {
    case bool(Bool)
    case float(Float)
    case integer(Int)
    case string(String)
}

extension MyValue : Codable {
    init(from theDecoder:Decoder) throws {
        let theEncodedValue = try theDecoder.singleValueContainer()

        if let theValue = try? theEncodedValue.decode(Bool.self) {
            self = .bool(theValue)
        } else if let theValue = try? theEncodedValue.decode(Float.self) {
            self = .float(theValue)
        } else if let theValue = try? theEncodedValue.decode(Int.self) {
            self = .integer(theValue)
        } else if let theValue = try? theEncodedValue.decode(String.self) {
            self = .string(theValue)
        } else { throw MyValueError.invalidEncoding }
    }

    func encode(to theEncoder:Encoder) throws {
        var theEncodedValue = theEncoder.singleValueContainer()
        switch self {
        case .bool(let theValue):
            try theEncodedValue.encode(theValue)
        case .float(let theValue):
            try theEncodedValue.encode(theValue)
        case .integer(let theValue):
            try theEncodedValue.encode(theValue)
        case .string(let theValue):
            try theEncodedValue.encode(theValue)
        }
    }
}

let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)

Однако это дает мне ошибку на этапе кодирования следующим образом:

 "Top-level MyValue encoded as number JSON fragment."

Проблема, по-видимому, заключается в том, что по какой-то причине JSONEncoder не позволяет кодировать тип верхнего уровня, который не является распознанным примитивом, как одно значение примитива. Если я изменю singleValueContainer() на unkeyedContainer(), то он будет работать нормально, за исключением того, что, конечно, результирующий JSON будет массивом, а не отдельным значением, или я могу использовать контейнер с ключом, но это создает объект с дополнительными накладными расходами ключа.

То, что я пытаюсь здесь сделать, невозможно с контейнером с одним значением? Если нет, есть ли обходной путь, который я могу использовать вместо этого?

Моя цель состояла в том, чтобы сделать мой тип Codable с минимальными накладными расходами, а не только как JSON (решение должно поддерживать любые допустимые Encoder/Decoder).


person Haravikk    schedule 09.05.2018    source источник
comment
Нет проблем с кодируемой реализацией в вашем типе (хотя вы должны поменять местами случаи с плавающей запятой/целое число, когда декодирование или любые целые числа будут пойманы в случае с плавающей запятой), JSONEncoder/Decoder просто не поддерживает кодирование объектов верхнего уровня, которые не являются массивом/словарем. Если, когда вы на самом деле используете этот тип, вы используете его как свойство другого кодируемого объекта, тогда он будет работать нормально.   -  person dan    schedule 09.05.2018
comment
Связано: stackoverflow. ком/вопросы/46768535/   -  person Martin R    schedule 09.05.2018
comment
Простым решением было бы обернуть значение в массив, например. let theEncodedValue = try! JSONEncoder().encode([MyValue.integer(123456)]), а затем расшифровать его с помощью let theDecodedValue = try! JSONDecoder().decode([MyValue].self, from: theEncodedValue)   -  person Guy Kogus    schedule 09.05.2018
comment
На самом деле, внесение этого изменения показывает, что @dan был прав, и он анализирует число как число с плавающей запятой, а не целое число.   -  person Guy Kogus    schedule 09.05.2018


Ответы (1)


Для этого есть отчет об ошибке:

https://bugs.swift.org/browse/SR-6163

SR-6163: JSONDecoder не может декодировать RFC 7159 JSON

По сути, начиная с RFC-7159, значение, подобное 123, является допустимым JSON, но JSONDecoder его не поддерживает. Вы можете следить за отчетом об ошибке, чтобы увидеть любые будущие исправления по этому поводу.

Где это терпит неудачу

Он терпит неудачу в следующей строке кода, где вы можете видеть, что если объект не является ни массивом, ни словарем, он не будет работать:

https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/JSONSerialization.swift#L120

open class JSONSerialization : NSObject {
        //...

        // top level object must be an Swift.Array or Swift.Dictionary
        guard obj is [Any?] || obj is [String: Any?] else {
            return false
        }

        //...
} 

Обходной путь

Вы можете использовать JSONSerialization с опцией: .allowFragments:

let jsonText = "123"
let data = Data(jsonText.utf8)

do {
    let myString = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
    print(myString)
}
catch {
    print(error)
}

Кодирование пар ключ-значение

Наконец, вы также можете сделать так, чтобы ваши объекты JSON выглядели так:

{ "integer": 123456 }

or

{ "string": "potatoe" }

Для этого вам нужно будет сделать что-то вроде этого:

import Foundation 

enum MyValue {
    case integer(Int)
    case string(String)
}

extension MyValue: Codable {

    enum CodingError: Error { 
        case decoding(String) 
    }

    enum CodableKeys: String, CodingKey { 
        case integer
        case string 
    }

    init(from decoder: Decoder) throws {

        let values = try decoder.container(keyedBy: CodableKeys.self)

        if let integer = try? values.decode(Int.self, forKey: .integer) {
            self = .integer(integer)
            return
        }

        if let string = try? values.decode(String.self, forKey: .string) {
            self = .string(string)
            return
        }

        throw CodingError.decoding("Decoding Failed")
    }


    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodableKeys.self)

        switch self {
            case let .integer(i):
            try container.encode(i, forKey: .integer)
            case let .string(s):
            try container.encode(s, forKey: .string)
        }
    }

}

let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
print(theEncodedString!) // { "integer": 123456 }
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)
person R.B.    schedule 09.05.2018
comment
Эк, тогда это классический момент фейспалма; конечно, этот тип не предназначен для использования в качестве элемента верхнего уровня, поэтому проблема связана с моим тестом, а не с реализацией. Спасибо за отличный ответ! - person Haravikk; 09.05.2018
comment
О, как примечание; Я попробовал вариант ключ/значение и нашел изящный способ сделать это. Вытягивая values.allKeys.first, вы фактически можете использовать переключатель для немедленного выполнения правильного декодирования, а не пробовать их все по одному, что особенно удобно во многих случаях; Мне просто не нравились накладные расходы на добавление ключа в закодированный формат. - person Haravikk; 09.05.2018
comment
Потрясающий. Спасибо за примечание! - person R.B.; 09.05.2018