skip to Main Content

Example, I often see this sort of thing,

                "Length": 0,
                "TestDate": {
                    "$date": "2023-09-20T05:00:00.0000000Z"
                },
                "Weight": 0,
                "Height": 0,

This works,

    struct DollarDate: Codable {
        let date: Date
        
        private enum CodingKeys: String, CodingKey {
            case date = "$date"
        }
    }

however, with Swift these days it’s usually not necessary to fuss with coding keys.

For example, if a key is a Swift keyword, you can just …

struct Draft: Codable {
    
    let id: String
    var `case`: Case?   // no problem these days, just use backtick
}

Is there a similar instant solution to problem key names like " $date " in json?

2

Answers


  1. I gather that your question is whether you could do something equivalent to:

    struct Foo {
        let `$bar`: Int          // “Cannot declare entity named '$bar'; the '$' prefix is reserved for implicitly-synthesized declarations”
    }
    

    The short answer is, no, you cannot.


    See The Swift Programming Language: Lexical Structure: Identifiers for rules regarding identifiers. Specifically, the leading $ has a unique purpose in Swift and is explicitly prohibited when declaring our own identifiers. As the aforementioned documentation says:

    The compiler synthesizes identifiers that begin with a dollar sign ($) for properties that have a property wrapper projection. Your code can interact with these identifiers, but you can’t declare identifiers with that prefix.

    CodingKeys is the right way to map a JSON key to something that is syntactically valid (or, more generally, any time we want to map JSON keys to any identifier, even if it is just for stylistic consistency) in a Swift codebase.

    Login or Signup to reply.
  2. There’s no trivial solution to your query, but if you run into this "dollar" problem often enough that it becomes cumbersome to keep writing coding keys, you could use a custom key decoding strategy.

    struct DollarKeyDecodingStrategy {
        static func call(_ codingPath: [CodingKey]) -> CodingKey {
            // we'll always have at least one element, so it's safe to force unwrap
            let lastComponent = codingPath.last!.stringValue
            if (lastComponent.starts(with: "$")) {
                return AnyKey(stringValue: String(lastComponent.dropFirst()))!
            } else {
                // initializer never fails, safe to unwrap here too
                return AnyKey(stringValue: lastComponent)!
            }
        }
    }
    
    struct AnyKey: CodingKey {
        var stringValue: String
        var intValue: Int?
        
        init?(stringValue: String) {
            self.stringValue = stringValue
            self.intValue = nil
        }
        
        init?(intValue: Int) {
            self.stringValue = String(intValue)
            self.intValue = intValue
        }
    }
    

    With the above in place, it’s just a matter of assigning a custom decoding strategy:

    let decoder = JSONDecoder()
    decoder.keyDecodingStrategy = .custom(DollarKeyDecodingStrategy.call)
    
    let dollarDate = decoder.decode(DollarDate.self, from: data)
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search