swiftencodingcodableencodable

Encode dictionary without adding the coding key enum in Swift


I want to encode a JSON that could be

{"hw1":{"get_trouble":true},"seq":2,"session_id":1}

or

{"hw2":{"get_trouble":true},"seq":3,"session_id":2}

the class for encoding looks like the following

class Request: Codable {
    let sessionId, seq:Int
    let content:[String:Content]
    
    enum CodingKeys:String, CodingKey{
        case sessionId = "session_id"
        case seq
        case content
    }
    
    init(sessionId:Int, seq:Int, content:[String:Content]) {
        self.sessionId = sessionId
        self.seq = seq
        self.content = content
    }
}

class Content:Codable{
    let getTrouble = true
    
    enum CodingKeys:String, CodingKey {
        case getTrouble = "get_trouble"
    }
}

how can I encode the request so that I can get the desired result? Currently, if I do

let request = Request(sessionId: session, seq: seq, content: [type:content])
let jsonData = try! encoder.encode(request)

I get

{"content":{"hw1":{"get_trouble":true}},"seq":2,"session_id":1}

and I don't want "content" inside the JSON. Already looked into
Swift Codable: encode structure with dynamic keys and couldn't figure out how to apply in my use case


Solution

  • As with almost all custom encoding problems, the tool you need is AnyStringKey (it frustrates me that this isn't in stdlib):

    struct AnyStringKey: CodingKey, Hashable, ExpressibleByStringLiteral {
        var stringValue: String
        init(stringValue: String) { self.stringValue = stringValue }
        init(_ stringValue: String) { self.init(stringValue: stringValue) }
        var intValue: Int?
        init?(intValue: Int) { return nil }
        init(stringLiteral value: String) { self.init(value) }
    }
    

    This just lets you encode and encode arbitrary keys. With this, the encoder is straightforward:

    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: AnyStringKey.self)
        for (key, value) in content {
            try container.encode(value, forKey: AnyStringKey(key))
        }
        try container.encode(sessionId, forKey: AnyStringKey("session_id"))
        try container.encode(seq, forKey: AnyStringKey("seq"))
    }
    

    This assumes you mean to allow multiple key/value pairs in Content. I expect you don't; you're just using a dictionary because you want a better way to encode. If Content has a single key, then you can rewrite it a bit more naturally this way:

    // Content only encodes getTrouble; it doesn't encode key
    struct Content:Codable{
        let key: String
        let getTrouble: Bool
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.singleValueContainer()
            try container.encode(["get_trouble": getTrouble])
        }
    }
    
    struct Request: Codable {
        // ...
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: AnyStringKey.self)
            try container.encode(content, forKey: AnyStringKey(content.key))
            try container.encode(sessionId, forKey: AnyStringKey("session_id"))
            try container.encode(seq, forKey: AnyStringKey("seq"))
        }
    }
    

    Now that may still bother you because it pushes part of the Content encoding logic into Request. (OK, maybe it just bothers me.) If you put aside Codable for a moment, you can fix that too.

    // Encode Content directly into container
    extension KeyedEncodingContainer where K == AnyStringKey {
        mutating func encode(_ value: Content) throws {
            try encode(["get_trouble": value.getTrouble], forKey: AnyStringKey(value.key))
        }
    }
    
    
    struct Request: Codable {
        // ...
    
        func encode(to encoder: Encoder) throws {
            var container = encoder.container(keyedBy: AnyStringKey.self)
    
            // And encode into the container (note no "forKey")
            try container.encode(content)
    
            try container.encode(sessionId, forKey: AnyStringKey("session_id"))
            try container.encode(seq, forKey: AnyStringKey("seq"))
        }
    }