How do I use custom keys with Swift 4's Decodable protocol?

匿名 (未验证) 提交于 2019-12-03 01:55:01

问题:

Swift 4 introduced support for native JSON encoding and decoding via the Decodable protocol. How do I use custom keys for this?

E.g., say I have a struct

struct Address:Codable {     var street:String     var zip:String     var city:String     var state:String } 

I can encode this to JSON.

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")  if let encoded = try? encoder.encode(address) {     if let json = String(data: encoded, encoding: .utf8) {         // Print JSON String         print(json)          // JSON string is             { "state":"California",               "street":"Apple Bay Street",               "zip":"94608",               "city":"Emeryville"             }     } } 

I can encode this back to an object.

    let newAddress: Address = try decoder.decode(Address.self, from: encoded) 

But If I had a json object that was

{     "state":"California",     "street":"Apple Bay Street",     "zip_code":"94608",     "city":"Emeryville"  } 

How would I tell the decoder on Address that zip_code maps to zip? I believe you use the new CodingKey protocol, but I can't figure out how to use this.

回答1:

Manually customising coding keys

In your example, you're getting an auto-generated conformance to Codable as all your properties also conform to Codable

However one really neat feature of this auto-generated conformance is that if you define a nested enum in your type called "CodingKeys" (or use a typealias with this name) that conforms to the CodingKeythis as the key type. This therefore allows you to easily customise the keys that your properties are encoded/decoded with.

So what this means is you can just say:

struct Address : Codable {      var street: String     var zip: String     var city: String     var state: String      private enum CodingKeys : String, CodingKey {         case street, zip = "zip_code", city, state     } } 

The enum case names need to match the property names, and the raw values of these cases need to match the keys that you're encoding to/decoding from (unless specified otherwise, the raw values of a String enumeration will the same as the case names). Therefore, the zip property will now be encoded/decoded using the key "zip_code".

The exact rules for the auto-generated Encodable/Decodable conformance are detailed by the evolution proposal (emphasis mine):

In addition to automatic CodingKey requirement synthesis for enums, Encodable & Decodable requirements can be automatically synthesized for certain types as well:

  1. Types conforming to Encodable whose properties are all Encodable get an automatically generated String-backed CodingKey enum mapping properties to case names. Similarly for Decodable types whose properties are all Decodable

  2. Types falling into (1) ― and types which manually provide a CodingKey enum (named CodingKeys, directly, or via a typealias) whose cases map 1-to-1 to Encodable/Decodable properties by name ― get automatic synthesis of init(from:) and encode(to:) as appropriate, using those properties and keys

  3. Types which fall into neither (1) nor (2) will have to provide a custom key type if needed and provide their own init(from:) and encode(to:), as appropriate

Example encoding:

import Foundation  let address = Address(street: "Apple Bay Street", zip: "94608",                       city: "Emeryville", state: "California")  do {     let encoded = try JSONEncoder().encode(address)     print(String(decoding: encoded, as: UTF8.self)) } catch {     print(error) } //{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"} 

Example decoding:

// using the """ multi-line string literal here, as introduced in SE-0168, // to avoid escaping the quotation marks let jsonString = """ {"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"} """  do {     let decoded = try JSONDecoder().decode(Address.self, from: Data(jsonString.utf8))     print(decoded) } catch {     print(error) }  // Address(street: "Apple Bay Street", zip: "94608", // city: "Emeryville", state: "California") 

Automatic snake_case JSON keys for camelCase property names

In Swift 4.1 (available in Xcode 9.3 beta), if you rename your zip property to zipCode, you can take advantage of the key encoding/decoding strategies on JSONEncoder and JSONDecoder in order to automatically convert coding keys between camelCase and snake_case.

Example encoding:

import Foundation  struct Address : Codable {   var street: String   var zipCode: String   var city: String   var state: String }  let address = Address(street: "Apple Bay Street", zipCode: "94608",                       city: "Emeryville", state: "California")  do {   let encoder = JSONEncoder()   encoder.keyEncodingStrategy = .convertToSnakeCase   let encoded = try encoder.encode(address)   print(String(decoding: encoded, as: UTF8.self)) } catch {   print(error) } //{"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}

Example decoding:

let jsonString = """ {"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"} """  do {   let decoder = JSONDecoder()   decoder.keyDecodingStrategy = .convertFromSnakeCase   let decoded = try decoder.decode(Address.self, from: Data(jsonString.utf8))   print(decoded) } catch {   print(error) }  // Address(street: "Apple Bay Street", zipCode: "94608", // city: "Emeryville", state: "California")

One important thing to note about this strategy however is that it won't be able to round-trip some property names with acronyms or initialisms which, according to the Swift API design guidelines, should be uniformly upper or lower case (depending on the position).

For example, a property named someURL will be encoded with the key some_url, but on decoding, this will be transformed to someUrl.

To fix this, you'll have to manually specify the coding key for that property to be string that the decoder expects, e.g someUrl in this case (which will still be transformed to some_url by the encoder):

struct S : Codable {    private enum CodingKeys : String, CodingKey {     case someURL = "someUrl", someOtherProperty   }    var someURL: String   var someOtherProperty: String } 

(This doesn't strictly answer your specific question, but given the canonical nature of this Q&A, I feel it's worth including)

Custom automatic JSON key mapping

In Swift 4.1 (available in Xcode 9.3 beta), you can take advantage of the custom key encoding/decoding strategies on JSONEncoder and JSONDecoder, allowing you to provide a custom function to map coding keys.

The function you provide takes a [CodingKey], which represents the coding path for the current point in encoding/decoding (in most cases, you'll only need to consider the last element; that is, the current key). The function returns a CodingKey that will replace the last key in this array.

For example, UpperCamelCase JSON keys for lowerCamelCase property names:

import Foundation  // wrapper to allow us to substitute our mapped string keys. struct AnyCodingKey : CodingKey {    var stringValue: String   var intValue: Int?    init(_ base: CodingKey) {     self.init(stringValue: base.stringValue, intValue: base.intValue)   }    init(stringValue: String) {     self.stringValue = stringValue   }    init(intValue: Int) {     self.stringValue = "\(intValue)"     self.intValue = intValue   }    init(stringValue: String, intValue: Int?) {     self.stringValue = stringValue     self.intValue = intValue   } } 

extension JSONEncoder.KeyEncodingStrategy {    static var convertToUpperCamelCase: JSONEncoder.KeyEncodingStrategy {     return .custom { codingKeys in        var key = AnyCodingKey(codingKeys.last!)        // uppercase first letter       if let firstChar = key.stringValue.first {         let i = key.stringValue.startIndex         key.stringValue.replaceSubrange(           i ... i, with: String(firstChar).uppercased()         )       }       return key     }   } } 

extension JSONDecoder.KeyDecodingStrategy {    static var convertFromUpperCamelCase: JSONDecoder.KeyDecodingStrategy {     return .custom { codingKeys in        var key = AnyCodingKey(codingKeys.last!)        // lowercase first letter       if let firstChar = key.stringValue.first {         let i = key.stringValue.startIndex         key.stringValue.replaceSubrange(           i ... i, with: String(firstChar).lowercased()         )       }       return key     }   } } 

You can now encode with the .convertToUpperCamelCase key strategy:

let address = Address(street: "Apple Bay Street", zipCode: "94608",                       city: "Emeryville", state: "California")  do {   let encoder = JSONEncoder()   encoder.keyEncodingStrategy = .convertToUpperCamelCase   let encoded = try encoder.encode(address)   print(String(decoding: encoded, as: UTF8.self)) } catch {   print(error) } //{"Street":"Apple Bay Street","City":"Emeryville","State":"California","ZipCode":"94608"} 

and decode with the .convertFromUpperCamelCase key strategy:

let jsonString = """ {"Street":"Apple Bay Street","City":"Emeryville","State":"California","ZipCode":"94608"} """  do {   let decoder = JSONDecoder()   decoder.keyDecodingStrategy = .convertFromUpperCamelCase   let decoded = try decoder.decode(Address.self, from: Data(jsonString.utf8))   print(decoded) } catch {   print(error) }  // Address(street: "Apple Bay Street", zipCode: "94608", // city: "Emeryville", state: "California") 


回答2:

When you declare a struct that conforms to Codable (Decodable and Encodable protocols) with the following implementation...

struct Address: Codable {     var street: String     var zip: String     var city: String     var state: String         } 

... the compiler automatically generates a nested enum that conforms to CodingKey protocol for you.

struct Address: Codable {     var street: String     var zip: String     var city: String     var state: String      // compiler generated     private enum CodingKeys: String, CodingKey {         case street         case zip         case city         case state     } } 

Therefore, If the keys used in your serialized data format don't match the property names from your data type, you have to manually implement this enum and set the appropriate rawValue for the required cases:

import Foundation  struct Address: Codable {     var street: String     var zip: String     var city: String     var state: String      private enum CodingKeys: String, CodingKey {         case street         case zip = "zip_code"         case city         case state     } } 

Usage #1: encode an Address instance into a JSON string

let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")  let encoder = JSONEncoder() if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {     print(jsonString) }  /*  prints:  {"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"}  */ 

Usage #2: decode a JSON string into an Address instance

let jsonString = """ {"state":"California","street":"Apple Bay Street","zip_code":"94608","city":"Emeryville"} """  let decoder = JSONDecoder() if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {     print(address) }  /*  prints:  Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")  */ 

Sources:



标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!