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?

鸡肉味嘎嘣脆
  • 233
  • 1
  • 3
  • 15

2 Answers2

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))
Timofey Solonin
  • 1,393
  • 13
  • 20
  • 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. `Date`s or `Data`), it won’t get applied. – Itai Ferber Jan 31 '18 at 14:48
  • @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. – Timofey Solonin Jan 31 '18 at 14:52
  • Furthermore if you need to break the encapsulation, you can type cast `value` parameter of the `AnyEncodable`. – Timofey Solonin Jan 31 '18 at 14:53
  • 1
    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_`](https://github.com/apple/swift/blob/master/stdlib/public/SDK/Foundation/JSONEncoder.swift#L818): 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. – Itai Ferber Jan 31 '18 at 15:42
  • 1
    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. – Itai Ferber Jan 31 '18 at 15:42
  • 1
    You can see this behavior in [this gist](https://gist.github.com/itaiferber/3a504c6abdc7d4e390815e0866a83782): 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. – Itai Ferber Jan 31 '18 at 15:44
  • @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. – Timofey Solonin Jan 31 '18 at 20:06
  • 1
    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 `Date`s 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. – Itai Ferber Jan 31 '18 at 21:39
  • @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. – Timofey Solonin Feb 01 '18 at 09:54
-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

Guy Kogus
  • 7,251
  • 1
  • 27
  • 32
  • 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. – Guy Kogus Jan 31 '18 at 14:43