JSONEncoder не позволяет типу, закодированному в примитивное значение - PullRequest
0 голосов
/ 09 мая 2018

Я работаю над реализацией 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).

1 Ответ

0 голосов
/ 09 мая 2018

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

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 }

или

{ "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)
...