How to encode Dictionary with JSONEncoder in Swift 4
Asked Answered
S

2

6

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?

Sonyasoo answered 31/1, 2018 at 14:6 Comment(4)
Why not use JSONSerializer instead? Do you want to prevent Any dependencies?Orchard
Yes I need the params as [String: Any]Estis
What are the types that can end up as values in the dictionary? It’s like not truly Any but one of several know types, right? Often the best solution to this is to make an enum with those types as associated values which confirms to Encodable.Acriflavine
Finally I add associatetype in RequestEstis
O
3

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))
Orchard answered 31/1, 2018 at 14:35 Comment(9)
Unfortunately, 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. Dates or Data), it won’t get applied.Acriflavine
@ItaiFerber AnyEncodable decorates the value of Encodable it receives. It uses implementation defined by the Encodable 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.Orchard
Furthermore if you need to break the encapsulation, you can type cast value parameter of the AnyEncodable.Orchard
When you encode(...) a value through one of JSONEncoder'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 of box_: it checks for specific types to apply. However, when you do value.encode(to: encoder), you reverse the relationship, and call the underlying type's implementation directly.Acriflavine
For instance, with Date, you end up encoding the date as a Double always, since that's how Date encodes by default (effectively, the code asks "Date, please encode yourself into the encoder", instead of "Encoder, please encode this date"). The encoder never saw that there was a Date since Date.encode is called directly, which just encodes the time interval value.Acriflavine
You can see this behavior in this gist: wrapping up the date in AnyEncodable loses the type context so the encoder can't apply the DateEncodingStrategy. 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.Acriflavine
@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 and FormattedDate for printing date differently upon the encoder. They can change the state of the encoder in the encode method, apply themselves and return encoder back to its original state.Orchard
There'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 Dates and not StandardDate or FormattedDate, there's nothing you can do. We offer these strategies for a very limited set of types (just Date and Data for now) because of this tradeoff.Acriflavine
@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.Orchard
A
-1

If you want to define your struct as conforming to Codable, you can do it like this:

struct Model: Codable {
    var param1: String
    var param2: Int
}

let model = Model(param1: "test", param2: 0)
let encoded = try? JSONEncoder().encode(model)
let decoded = try? JSONDecoder().decode(Model.self, from: encoded!)

It won't really work if you set params: [String: Any] because the encoders/decoders don't know how to encode/decode Any, but they can do it for the primitive types.

If you want more help, you should read more about the new Codable protocol. I recommend this: https://hackernoon.com/everything-about-codable-in-swift-4-97d0e18a2999

Augmenter answered 31/1, 2018 at 14:36 Comment(1)
Timofey 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.Augmenter

© 2022 - 2025 — McMap. All rights reserved.