2

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

Muhammad Rafay
  • 133
  • 3
  • 7
  • Do you also need decoding? – Sweeper Jan 09 '22 at 18:27
  • Yes, a similar request will need to be decoded. It could be {"seq":1,"hw1":{"get_trouble":{"troubles":["alarm"],"error_code":0}}} {"seq":1,"hw2":{"get_trouble":{"troubles":["bus"],"error_code":0}}} – Muhammad Rafay Jan 09 '22 at 18:32
  • The json in your comment doesn’t look even close to the types you have in your code. – Joakim Danielson Jan 09 '22 at 19:02
  • Sorry but I couldn’t format it properly. The json in comment is the response which I need to decode later. Currently I am trying to encode the request that I need to send to the server and I’ll follow the same approach for decoding the response – Muhammad Rafay Jan 09 '22 at 19:25

1 Answers1

2

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"))
    }
}
Rob Napier
  • 286,113
  • 34
  • 456
  • 610
  • Can you add details to explain how AnyStringKey works? – Merlin -they-them- Mar 14 '23 at 01:11
  • CodingKey is just a protocol, and AnyStringKey is just a struct that conforms to it. There's nothing more to it. Is there more to the question? – Rob Napier Mar 15 '23 at 02:39
  • What are the Hashable and ExpressibleByStringLiteral for? What's with all the different init functions? Why have intValue but not assign it anywhere? Saying something is just a protocol and something conforms to it is not descriptive about why it works or how it works. – Merlin -they-them- Mar 16 '23 at 03:06
  • It's not complex, but if you're curious about how these tools work, see https://youtu.be/-k_vipGhugQ?t=507 I go reasonably deep into CodingKey there (deeper than most people are likely interested in.) – Rob Napier Mar 16 '23 at 13:29
  • I can watch that video sure, but I'm also asking for Stack Overflow purposes. A good answer is self-contained with references. So the answer to these questions should be in this answer – Merlin -they-them- Mar 17 '23 at 14:20