简体   繁体   中英

Is there a shorter way of declaring CodingKeys?

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 ...
}

Edit: I guess this is not possible with the current Swift version, but does anyone know if this would somehow be included in the future versions of Swift?

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! :)

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM