Home > other >  Is there a shorter way of declaring CodingKeys?
Is there a shorter way of declaring CodingKeys?

Time:06-27

Say you have a struct for a model of your API response. Let's say it has 50 members. However, 5-7 members are non-standard casing, you could have AUsernAme or _BTmember, but the rest are all snake case credit_score or status_code.

Rather than writing all members like this:

struct MyStruct {
  let aUserName: String
  //  50 more...

  private enum CodingKeys: String, CodingKey {
    case aUserName = "AUsernAme"
    //  50 more...
  }
}

Is there a way that we can write it like this?

struct MyStruct {
  @CodingKey("AUsernAme") let aUserName: String
  let creditScore: Int
  //  50 more ...
}

CodePudding user response:

The solution which Sweeper provided is a great solution to your problem, but IMO it may display great complexity to your problem and to the next developers who will read this code.

If I were you, I would just stick to writing all the CodingKeys for simplicity. If your worry is writing a lot of lines of cases, you can write all the cases that doesn't need custom keys in one line and just add the keys with unusual/non-standard casing on new lines:

case property1, property2, property3, property4, property5...
case property50 = "_property50"

And since you mentioned that the rest are in snake case, not sure if you know yet, but we have JSONDecoder.KeyDecodingStrategy.convertFromSnakeCase.

Hope this helps `tol! :)

CodePudding user response:

How about setting a custom keyDecodingStrategy just before you decode instead?

struct AnyCodingKey: CodingKey, Hashable {
    var stringValue: String

    init(stringValue: String) {
        self.stringValue = stringValue
    }

    var intValue: Int?

    init(intValue: Int) {
        self.intValue = intValue
        self.stringValue = "\(intValue)"
    }
}

let mapping = [
    "AUsernAme": "aUserName",
    // other mappings...
]

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ codingPath in
    let key = codingPath[0].stringValue
    guard let mapped = mapping[key] else { return codingPath.last! }
    return AnyCodingKey(stringValue: mapped)
})

This assumes your JSON has a single level flat structure. You can make this into an extension:

extension JSONDecoder.KeyDecodingStrategy {
    static func mappingRootKeys(_ dict: [String: String]) -> JSONDecoder.KeyDecodingStrategy {
        return .custom { codingPath in
            let key = codingPath[0].stringValue
            guard let mapped = dict[key] else { return codingPath.last! }
            return AnyCodingKey(stringValue: mapped)
        }
    }
}

let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .mappingRootKeys(mapping)

If your JSON has more levels, you can change the type of the dictionary to [JSONPath: String], where JSONPath is a type that you can create that represents a key in a nested JSON. Then add a bit of code that converts the coding path, which is just an array of coding keys, to JSONPath. This should not be hard to write on your own.

A simple way is to just use [AnyCodingKey] as JSONPath, but there are many other ways too, and I encourage you to experiment and find the one you like the best.

typealias JSONPath = [AnyCodingKey]

extension AnyCodingKey {
    init(codingKey: CodingKey) {
        if let int = codingKey.intValue {
            self.init(intValue: int)
        } else {
            self.init(stringValue: codingKey.stringValue)
        }
    }
}


extension JSONDecoder.KeyDecodingStrategy {
    static func mappingRootKeys(_ dict: [JSONPath: String]) -> JSONDecoder.KeyDecodingStrategy {
        return .custom { codingPath in
            guard let mapped = dict[codingPath.map(AnyCodingKey.init(codingKey:))] else { return codingPath.last! }
            return AnyCodingKey(stringValue: mapped)
        }
    }
}

let mapping = [
    [AnyCodingKey(stringValue: "AUsernAme")]: "aUserName"
]

It is not possible to use a property wrapper for this. Your property wrapper @CodingKey("AUsernAme") let aUserName: String will be compiled to something like this (as per here):

private var _aUserName: CodingKey<String> = CodingKey("AUsernAme") 
var aUserName: String {
    get { _aUserName.wrappedValue }
    set { _aUserName.wrappedValue = newValue }
}

There are two main problems with this:

  • Assuming you don't want to write init(from:) for all the 50 properties in MyStruct, code will be synthesised to decode it, assigning to its _aUserName property. You only have control over the init(from:) initialiser of the CodingKey property wrapper, and you cannot do anything about how MyStruct is decoded in there. If MyStruct is contained in another struct:

      struct AnotherStruct: Decodable {
          let myStruct: MyStruct
      }
    

    Then you can indeed control the coding keys used to decode myStruct by marking it with a property wrapper. You can do whatever you want in the decoding process by implementing the property wrapper's init(from:), which brings us to the second problem:

  • The coding key you pass to the CodingKey property wrapper is passed via an initialiser of the form init(_ key: String). But you control the decoding via the initialiser init(from decoder: Decoder) because that is what will be called when the struct is decoded. In other words, there is no way for you to send the key mappings to the property wrapper.

  • Related