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
CodePudding user response:
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"))
}
}
CodePudding user response:
First of all use structs rather then classes.
The only way to encode the data structure is to add a parameter to the init method to determine whether it's hw1 or hw2 and implement encode(to encoder, something like this
struct Request: Encodable {
let sessionId, seq: Int
let content: Content
let isHW1 : Bool
enum CodingKeys:String, CodingKey{
case sessionId = "session_id"
case seq, content
case hw1, hw2
}
init(sessionId: Int, seq: Int, content: Content, isHW1: Bool) {
self.sessionId = sessionId
self.seq = seq
self.content = content
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
if isHW1 {
try container.encode(content, forKey: .hw1)
} else {
try container.encode(content, forKey: .hw2)
}
try container.encode(seq, forKey: .seq)
try container.encode(sessionId, forKey: .sessionId)
}
}
struct Content : Codable{
let getTrouble = true
enum CodingKeys:String, CodingKey {
case getTrouble = "get_trouble"
}
}
let request = Request(sessionId: session, seq: seq, content: content, isHW1: true)
let jsonData = try! encoder.encode(request)
To decode the stuff implement also init(for decoder. Then try to decode hw1. If this fails decode hw2
