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))
Share:
11,394
鸡肉味嘎嘣脆
Author by

鸡肉味嘎嘣脆

Want to be a full-stack developer.

Updated on June 15, 2022

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
    Guy Kogus over 6 years
    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.
  • Itai Ferber
    Itai Ferber over 6 years
    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.
  • Timofey Solonin
    Timofey Solonin over 6 years
    @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
    Timofey Solonin over 6 years
    Furthermore if you need to break the encapsulation, you can type cast value parameter of the AnyEncodable.
  • Itai Ferber
    Itai Ferber over 6 years
    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.
  • Itai Ferber
    Itai Ferber over 6 years
    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
    Itai Ferber over 6 years
    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.
  • Timofey Solonin
    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 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.
  • Itai Ferber
    Itai Ferber over 6 years
    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.
  • Timofey Solonin
    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.