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
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"))
}
}