I'm working on an implementation of Codable
for an enum
type with possible associated values. Since these are unique to each case, I thought I could get away with outputting them without keys during encoding, and then simply see what I can get back when decoding in order to restore the correct case.
Here's a very much trimmed down, contrived example demonstrating a sort of dynamically typed value:
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)
However, this is giving me an error during the encoding stage as follows:
"Top-level MyValue encoded as number JSON fragment."
The issue appears to be that, for whatever reason, the JSONEncoder
won't allow a top-level type that isn't a recognised primitive to be encoded as a single primitive value. If I change the singleValueContainer()
to an unkeyedContainer()
then it works just fine, except that of course the resulting JSON
is an array, not a single value, or I can use a keyed container but this produces an object with the added overhead of a key.
Is what I'm trying to do here impossible with a single value container? If not, is there some workaround that I can use instead?
My aim was to make my type Codable
with a minimum of overhead, and not just as JSON
(the solution should support any valid Encoder
/Decoder
).
An object that encodes instances of a data type as JSON objects.
I know that Any is not Codable . What I need to know is how can I make it work. Related: Swift structures handling multiple tapes for a single property. In summary: you shouldn't use 'Any', but have 2 optional properties (one of type 'String' and one 'Int' in your case) and try decoding the JSON value as both.
Judging by the code for JSONDecoder and JSONEncoder they seem to be thread-safe. Both encode() and decode() use JSONSerialization (which has been thread-safe since iOS 7 and macOS 10.9), and both methods create their own private decode/encode objects in local variables.
Remember, Swift's String , Int , and Bool are all Codable ! Earlier I wrote that your structs, enums, and classes can conform to Codable . Swift can generate the code needed to extract data to populate a struct's properties from JSON data as long as all properties conform to Codable .
There is a bug report for this:
https://bugs.swift.org/browse/SR-6163
SR-6163: JSONDecoder cannot decode RFC 7159 JSON
Basically, since RFC-7159, a value like 123
is valid JSON, but JSONDecoder
won't support it. You may follow up on the bug report to see any future fixes on this. [The bug was fixed starting in iOS 13.]
#Where it fails#
It fails in the following line of code, where you can see that if the object is not an array nor dictionary, it will fail:
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
}
//...
}
#Workaround#
You may use JSONSerialization
, with the option: .allowFragments
:
let jsonText = "123"
let data = Data(jsonText.utf8)
do {
let myString = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(myString)
}
catch {
print(error)
}
Finally, you could also have your JSON objects look like this:
{ "integer": 123456 }
or
{ "string": "potatoe" }
For this, you would need to do something like this:
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With