How to encode Dictionary with JSONEncoder in Swift 4
11,394
You have to introduce type erasure as follows:
struct AnyEncodable: Encodable {
let value: Encodable
init(value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
struct Model: Encodable {
var params: [String: AnyEncodable]
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try! encoder.encode(
Model(
params: [
"hello" : AnyEncodable.init(value: "world")
]
).params
)
print(String(data: json, encoding: .utf8))
![鸡肉味嘎嘣脆](https://lh5.googleusercontent.com/-qAPTLa_fYuY/AAAAAAAAAAI/AAAAAAAACnE/eLa4tdedppg/photo.jpg?sz=256)
Comments
-
鸡肉味嘎嘣脆 about 2 years
I want to encode Dictionary to json with JSONEncoder. It seems like a Request, receive a dictionary as parameter and encode it to json as http body. The code is looks like this:
let dict = ["name": "abcde"] protocol Request { var params: [String: Encodable] { get set } func encode<T>(_ value: T) throws -> Data where T : Encodable } extension Request { func encode<T>(_ value: T) throws -> Data where T : Encodable { return try JSONEncoder().encode(value) } var body: Data? { if let encoded = try? self.encode(self.params) { return encoded } return nil } } struct BaseRequest: Request { var params: [String : Encodable] } let req = BaseRequest(params: dict) let body = req.body
But this code occurs error
Fatal error: Dictionary<String, Encodable> does not conform to Encodable because Encodable does not conform to itself. You must use a concrete type to encode or decode.
How could I make this encodable?
-
Guy Kogus over 6 yearsTimofey Solonin's answer shows a nice way to encapsulate an
Encodable
Any
, in case you want to use that. However you'd be better off defining exactly the types used by your models so that you won't need to encapsulate them. -
Itai Ferber over 6 yearsUnfortunately, type erasure like this prevents the Encoder from intercepting the type before encoding. That means if you’re trying to encode a type which may have an encoding strategy (e.g.
Date
s orData
), it won’t get applied. -
Timofey Solonin over 6 years@ItaiFerber
AnyEncodable
decorates the value ofEncodable
it receives. It uses implementation defined by theEncodable
itself (which is defined differently for every concrete implementation). I don't see how it breaks different strategies of encoding unless you are trying to type cast which is a very bad idea. -
Timofey Solonin over 6 yearsFurthermore if you need to break the encapsulation, you can type cast
value
parameter of theAnyEncodable
. -
Itai Ferber over 6 yearsWhen you
encode(...)
a value through one ofJSONEncoder
's containers, it ends up boxing the value for encoding. Since all of the encode methods are generic, it knows the type of what's being encoded, and can intercept that to apply an encoding strategy. You can see this in the implementation ofbox_
: it checks for specific types to apply. However, when you dovalue.encode(to: encoder)
, you reverse the relationship, and call the underlying type's implementation directly. -
Itai Ferber over 6 yearsFor instance, with
Date
, you end up encoding the date as aDouble
always, since that's howDate
encodes by default (effectively, the code asks "Date, please encode yourself into the encoder", instead of "Encoder, please encode this date"). Theencoder
never saw that there was aDate
sinceDate.encode
is called directly, which just encodes the time interval value. -
Itai Ferber over 6 yearsYou can see this behavior in this gist: wrapping up the date in
AnyEncodable
loses the type context so the encoder can't apply theDateEncodingStrategy
. If you don't use strategies, then this is of course totally fine. It's just a caveat to be aware of so you don't end up with conflicting value formats inside the same encoded payload. -
Timofey Solonin over 6 years@ItaiFerber thanks for pointing that out! I didn't even know such behavior existed with the encoder. However mutating encoder is a bad idea anyway. Why no introduce two conceptual decorators like
StandardDate
andFormattedDate
for printing date differently upon the encoder. They can change the state of the encoder in theencode
method, apply themselves and return encoder back to its original state. -
Itai Ferber over 6 yearsThere's generally a tradeoff between correctness (i.e. not breaking encapsulation), and usefulness. JSON is very often sent off to servers which have strict requirements on the formats of dates (since JSON doesn't specify how dates must be encoded, every server is different), and very often, you need to encode types which you don't own and cannot affect — if those types encode
Date
s and notStandardDate
orFormattedDate
, there's nothing you can do. We offer these strategies for a very limited set of types (justDate
andData
for now) because of this tradeoff. -
Timofey Solonin over 6 years@ItaiFerber I see your reasoning but by introducing encapsulation breaking concepts we create a positive feedback loop where we have developers cutting corners instead of trying to design convenient OO concepts that will solve the constraint of strict encapsulation design.