Home > database >  Encode dictionary without adding the coding key enum in Swift
Encode dictionary without adding the coding key enum in Swift

Time:01-10

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

  •  Tags:  
  • Related