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.
Manually customising coding keys
In your example, you're getting an auto-generated conformance to Codable as all your properties also conform to Codable. This conformance automatically creates a key type that simply corresponds to the property names – which is then used in order to encode to/decode from a single keyed container.
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 CodingKey protocol – Swift will automatically use this 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:
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
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
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, 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, 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")
With Swift 4.2, according to your needs, you may use one of the 3 following strategies in order to make your model objects custom property names match your JSON keys.
#1. Using custom coding keys
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 can manually implement this enum and set the appropriate rawValue for the required cases.
The following example shows how to do:
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
}
}
Encode (replacing zip property with "zip_code" JSON key):
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"}
*/
Decode (replacing "zip_code" JSON key with zip property):
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")
*/
#2. Using snake case to camel case key coding strategies
If your JSON has snake-cased keys and you want to convert them to camel-cased properties for your model object, you can set your JSONEncoder's keyEncodingStrategy and JSONDecoder's keyDecodingStrategy properties to .convertToSnakeCase.
The following example shows how to do:
import Foundation
struct Address: Codable {
var street: String
var zipCode: String
var cityName: String
var state: String
}
Encode (converting camel cased properties into snake cased JSON keys):
let address = Address(street: "Apple Bay Street", zipCode: "94608", cityName: "Emeryville", state: "California")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .convertToSnakeCase
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_name":"Emeryville"}
*/
Decode (converting snake cased JSON keys into camel cased properties):
let jsonString = """
{"state":"California","street":"Apple Bay Street","zip_code":"94608","city_name":"Emeryville"}
"""
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
if let jsonData = jsonString.data(using: .utf8), let address = try? decoder.decode(Address.self, from: jsonData) {
print(address)
}
/*
prints:
Address(street: "Apple Bay Street", zipCode: "94608", cityName: "Emeryville", state: "California")
*/
#3. Using custom key coding strategies
If necessary, JSONEncoder and JSONDecoder allow you to set a custom strategy to map coding keys using JSONEncoder.KeyEncodingStrategy.custom(_:) and JSONDecoder.KeyDecodingStrategy.custom(_:).
The following example shows how to implement them:
import Foundation
struct Address: Codable {
var street: String
var zip: String
var city: String
var state: String
}
struct AnyKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.stringValue = String(intValue)
self.intValue = intValue
}
}
Encode (converting lowercased first letter properties into uppercased first letter JSON keys):
let address = Address(street: "Apple Bay Street", zip: "94608", city: "Emeryville", state: "California")
let encoder = JSONEncoder()
encoder.keyEncodingStrategy = .custom({ (keys) -> CodingKey in
let lastKey = keys.last!
guard lastKey.intValue == nil else { return lastKey }
let stringValue = lastKey.stringValue.prefix(1).uppercased() + lastKey.stringValue.dropFirst()
return AnyKey(stringValue: stringValue)!
})
if let jsonData = try? encoder.encode(address), let jsonString = String(data: jsonData, encoding: .utf8) {
print(jsonString)
}
/*
prints:
{"Zip":"94608","Street":"Apple Bay Street","City":"Emeryville","State":"California"}
*/
Decode (converting uppercased first letter JSON keys into lowercased first letter properties):
let jsonString = """
{"State":"California","Street":"Apple Bay Street","Zip":"94608","City":"Emeryville"}
"""
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .custom({ (keys) -> CodingKey in
let lastKey = keys.last!
guard lastKey.intValue == nil else { return lastKey }
let stringValue = lastKey.stringValue.prefix(1).lowercased() + lastKey.stringValue.dropFirst()
return AnyKey(stringValue: stringValue)!
})
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:
Apple developer documentation: "Encoding and Decoding Custom Types"
WWDC 2017 session 212: "What's new in Foundation"
MartianCraft: "Implementing a custom key strategy for coding types"
What I have done is create own structure just like what you are getting from the JSON with respect to its data types.
Just like this:
struct Track {
let id : Int
let contributingArtistNames:String
let name : String
let albumName :String
let copyrightP:String
let copyrightC:String
let playlistCount:Int
let trackPopularity:Int
let playlistFollowerCount:Int
let artistFollowerCount : Int
let label : String
}
After this you need to create an extension of the same struct extending decodable and the enum of the same structure with CodingKey and then you need to initialize the decoder using this enum with its keys and datatypes (Keys will come from the enum and the datatypes will be coming or say referenced from the structure itself)
extension Track: Decodable {
enum TrackCodingKeys: String, CodingKey {
case id = "id"
case contributingArtistNames = "primaryArtistsNames"
case spotifyId = "spotifyId"
case name = "name"
case albumName = "albumName"
case albumImageUrl = "albumImageUrl"
case copyrightP = "copyrightP"
case copyrightC = "copyrightC"
case playlistCount = "playlistCount"
case trackPopularity = "trackPopularity"
case playlistFollowerCount = "playlistFollowerCount"
case artistFollowerCount = "artistFollowers"
case label = "label"
}
init(from decoder: Decoder) throws {
let trackContainer = try decoder.container(keyedBy: TrackCodingKeys.self)
if trackContainer.contains(.id){
id = try trackContainer.decode(Int.self, forKey: .id)
}else{
id = 0
}
if trackContainer.contains(.contributingArtistNames){
contributingArtistNames = try trackContainer.decode(String.self, forKey: .contributingArtistNames)
}else{
contributingArtistNames = ""
}
if trackContainer.contains(.spotifyId){
spotifyId = try trackContainer.decode(String.self, forKey: .spotifyId)
}else{
spotifyId = ""
}
if trackContainer.contains(.name){
name = try trackContainer.decode(String.self, forKey: .name)
}else{
name = ""
}
if trackContainer.contains(.albumName){
albumName = try trackContainer.decode(String.self, forKey: .albumName)
}else{
albumName = ""
}
if trackContainer.contains(.albumImageUrl){
albumImageUrl = try trackContainer.decode(String.self, forKey: .albumImageUrl)
}else{
albumImageUrl = ""
}
if trackContainer.contains(.copyrightP){
copyrightP = try trackContainer.decode(String.self, forKey: .copyrightP)
}else{
copyrightP = ""
}
if trackContainer.contains(.copyrightC){
copyrightC = try trackContainer.decode(String.self, forKey: .copyrightC)
}else{
copyrightC = ""
}
if trackContainer.contains(.playlistCount){
playlistCount = try trackContainer.decode(Int.self, forKey: .playlistCount)
}else{
playlistCount = 0
}
if trackContainer.contains(.trackPopularity){
trackPopularity = try trackContainer.decode(Int.self, forKey: .trackPopularity)
}else{
trackPopularity = 0
}
if trackContainer.contains(.playlistFollowerCount){
playlistFollowerCount = try trackContainer.decode(Int.self, forKey: .playlistFollowerCount)
}else{
playlistFollowerCount = 0
}
if trackContainer.contains(.artistFollowerCount){
artistFollowerCount = try trackContainer.decode(Int.self, forKey: .artistFollowerCount)
}else{
artistFollowerCount = 0
}
if trackContainer.contains(.label){
label = try trackContainer.decode(String.self, forKey: .label)
}else{
label = ""
}
}
}
You need to change here each and every key and datatypes according to your needs and use it with the decoder.
By using CodingKey you can use custom keys in codable or decodable protocol.
struct person: Codable {
var name: String
var age: Int
var street: String
var state: String
private enum CodingKeys: String, CodingKey {
case name
case age
case street = "Street_name"
case state
} }
Related
I am trying to parse this json file
{
"Global Quote": {
"01. symbol": "AAPL",
"02. open": "122.6000",
"03. high": "123.3500",
"04. low": "121.5400",
"05. price": "121.7800",
"06. volume": "79075988",
"07. latest trading day": "2020-12-14",
"08. previous close": "122.4100",
"09. change": "-0.6300",
"10. change percent": "-0.5147%"
} }
I want to parse his JSON text but am not quite sure why my structure is not filling this global quote object but instead leaves it empty it empty.
struct GlobalQuote: Codable{
var symbol: String?
var open: String?
var high: String?
var low: String?
var price: String?
var volume: String?
var latestTradingDay: String?
var previousClose: String?
var change: String?
var changepercent: String?
private enum codingKeys : String, CodingKey {
case symbol = "01. symbol"
case open = "02. open"
case high = "03. high"
case low = "04. low"
case price = "05. price"
case volume = "06.volume"
case latestTradingDay = "07. latest trading day"
case previousClose = "08. previous close"
case change = "09. change"
case changepercent = "10. change percent"
}
}
...It is returns this in the console..
GlobalQuote(symbol: nil, open: nil, high: nil, low: nil, price: nil, volume: nil, latestTradingDay: nil, previousClose: nil, change: nil, changepercent: nil)
...Not really sure why I am not receiving my data.
To decode your JSON you first need to change the codingKeys inner enum of GlobalQuote struct to CodingKeys. (start with upper case)
Then you either need another struct to represent the outer JSON object like this:
struct ResponseGlobalQuote: Codable {
var globalQuote: GlobalQuote
enum CodingKeys: String, CodingKey {
case globalQuote = "Global Quote"
}
}
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(ResponseGlobalQuote.self, from: data)
print(decoded)
} catch {
print(error)
}
or modify your decoding to use [String: GlobalQuote] as the decoding type:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode([String: GlobalQuote].self, from: Data(jsonString.utf8))
print(decoded.values.first)
} catch {
print(error)
}
I am trying to parse the json. But the problem is I have json inside to it another json String. Like :
{
"count": 284,
"next": "http://X:X:X:X:8080/api/sensor/last5feed?page=2&search=XXXXX",
"previous": null,
"results": [
{
"id": 571,
"feed": "{'app_id': 'XXXXX', 'dev_id': 'XXXX', 'hardware_serial': 'XXXXX', 'port': 6, 'counter': 4290, 'payload_raw': 'AQEBIwXsF4IAAAA=', 'payload_fields': {'aamsg_type': 'weather', 'abstatus': 0, 'batteryV': 3.5, 'battery_low': 'no', 'humiP': 60.2, 'tempC': 15.2}, 'metadata': {'time': '2020-01-23T15:09:32.350967362Z', 'frequency': 868.1, 'modulation': 'LORA', 'data_rate': 'SF7BW125', 'airtime': 61696000, 'coding_rate': '4/5', 'gateways': [{'gtw_id': 'XXXXX', 'timestamp': 3227230963, 'time': '2020-01-23T15:09:32.326146Z', 'channel': 0, 'rssi': -98, 'snr': 4.8, 'rf_chain': 1, 'latitude': 57.124737, 'longitude': -2.1646452, 'altitude': 90, 'location_source': 'registry'}]}}",
"created_at": "2020-01-23T15:09:32.630326Z",
"sensor": 1
},
{
"id": 569,
"feed": "{'app_id': 'XXXXXX', 'dev_id': 'XXXX', 'hardware_serial': 'XXXX', 'port': 6, 'counter': 4289, 'payload_raw': 'XXXXX', 'payload_fields': {'aamsg_type': 'weather', 'abstatus': 0, 'batteryV': 3.5, 'battery_low': 'no', 'humiP': 57.6, 'tempC': 16.9}, 'metadata': {'time': '2020-01-23T14:09:32.132070865Z', 'frequency': 867.3, 'modulation': 'LORA', 'data_rate': 'SF7BW125', 'airtime': 61696000, 'coding_rate': '4/5', 'gateways': [{'gtw_id': 'XXXXXX', 'timestamp': 3921981659, 'time': '2020-01-23T14:09:32.104672Z', 'channel': 4, 'rssi': -107, 'snr': 8.2, 'rf_chain': 0, 'latitude': 57.124737, 'longitude': -2.1646452, 'altitude': 90, 'location_source': 'registry'}]}}",
"created_at": "2020-01-23T14:09:32.448929Z",
"sensor": 1
}
}
I am getting the values till feed. But I am not able to parse further, My code is :
if(status_code == 200){
if let json = response.data {
do{
let data = try JSON(data: json)
let result = data["results"].arrayObject! as NSArray
let ct = result.count
if(ct != 0 ) {
self.noDataFound.isHidden = true
for i in 0...ct-1 {
let data = result[i] as? NSDictionary
let feed = data?.value(forKey: "feed") as? NSString
let data3 = try JSON(data: feed as! Data) .
print(data3)
}
}
} catch {} }}
I need to get the hardware_serial from feed. Can any body please help me what i am doing wrong here!! Thanks!!!
The string for key feed is not valid JSON. You have to replace the single quotes with double quotes.
Create a Data object from the string (casting the type doesn't work).
Create a JSON object from the data.
Get the values you need.
Side note:
Don't use NS... collection types and NSString in Swift.
Use Codable to parse the above JSON response.
Models:
struct Root: Codable {
let count: Int
let next: String
let previous: String?
let results: [Result]
}
struct Result: Codable {
let id: Int
let feed, createdAt: String
let sensor: Int
}
Parse the JSON data like so,
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let response = try decoder.decode(Root.self, from: data)
print(response)
} catch {
print(error)
}
First, peel off the piece you want:
struct ResultWrapper: Decodable {
let results: [Result]
}
And then build a custom decoder to extract feed, which is malformed. Single-quotes are not legal JSON. As a hack, the following code just substitutes all single-quotes with double-quotes, but this won't work if there are any embedded quoted-single-quotes.
struct Result: Decodable {
enum CodingKeys: String, CodingKey { case feed }
let feed: Feed
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
// inner JSON is mal-formed. Have to fix it.
let feedString = try container.decode(String.self, forKey: .feed)
.replacingOccurrences(of: "'", with: "\"")
feed = try JSONDecoder().decode(Feed.self, from: Data(feedString.utf8))
}
}
Finally, decoding Feed is mechanical, but requires custom CodingKeys. I recommend quicktype for that:
// https://app.quicktype.io?share=7O6iNJ3ugXb4mr84TIoG
struct Feed: Codable {
var appID, devID, hardwareSerial: String
var port, counter: Int
var payloadRaw: String
var payloadFields: PayloadFields
var metadata: Metadata
enum CodingKeys: String, CodingKey {
case appID = "app_id"
case devID = "dev_id"
case hardwareSerial = "hardware_serial"
case port, counter
case payloadRaw = "payload_raw"
case payloadFields = "payload_fields"
case metadata
}
}
// MARK: - Metadata
struct Metadata: Codable {
var time: String
var frequency: Double
var modulation, dataRate: String
var airtime: Int
var codingRate: String
var gateways: [Gateway]
enum CodingKeys: String, CodingKey {
case time, frequency, modulation
case dataRate = "data_rate"
case airtime
case codingRate = "coding_rate"
case gateways
}
}
// MARK: - Gateway
struct Gateway: Codable {
var gtwID: String
var timestamp: Int
var time: String
var channel, rssi: Int
var snr: Double
var rfChain: Int
var latitude, longitude: Double
var altitude: Int
var locationSource: String
enum CodingKeys: String, CodingKey {
case gtwID = "gtw_id"
case timestamp, time, channel, rssi, snr
case rfChain = "rf_chain"
case latitude, longitude, altitude
case locationSource = "location_source"
}
}
// MARK: - PayloadFields
struct PayloadFields: Codable {
var aamsgType: String
var abstatus: Int
var batteryV: Double
var batteryLow: String
var humiP, tempC: Double
enum CodingKeys: String, CodingKey {
case aamsgType = "aamsg_type"
case abstatus, batteryV
case batteryLow = "battery_low"
case humiP, tempC
}
}
I want to convert a CollegeRequest type to a dictionary or data to send it to a server while requesting POST-API.
Here is my CollegeRequest type:
struct CollegeRequest: Codable {
struct Student: Codable {
var id: Int
var name: String
}
var student: Student
var date: Date
}
Once converted (along with the hierarchical keys), it should be like this:
{
"student.id": 3,
"student.name": "Raj",
"date": "2019-10-31T13:59:00+00:00"
}
How can I achieve this ?
Use Encodable protocol to encode your cutom struct type CollegeRequest. Your structure needs a manual encoding
If the structure of your Swift type differs from the structure of its
encoded form, you can provide a custom implementation of Encodable and
Decodable to define your own encoding and decoding logic. source
To encode the CollegeRequest to Data type, use JSONEncoder. An object that encodes instances of a data type as JSON objects.
Concerning the date stored property you you have to use JSONEncoder.DateEncodingStrategy.formatted(_:), the strategy that defers formatting settings to a supplied date formatter.
struct CollegeRequest: Codable {
struct Student: Codable {
var id: Int
var name: String
}
var student: Student
var date: Date
enum CodingKeys: String, CodingKey {
case studentId = "student.id"
case studentName = "student.name"
case date
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(student.id, forKey: .studentId)
try container.encode(student.name, forKey: .studentName)
try container.encode(date, forKey: .date)
}
init(student: CollegeRequest.Student, date: Date) {
self.student = student
self.date = date
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
let studentId = try values.decode(Int.self, forKey: .studentId)
let studentName = try values.decode(String.self, forKey: .studentName)
student = Student(id: studentId, name: studentName)
date = try values.decode(Date.self, forKey: .date)
}
}
Usage
let request = CollegeRequest(student: CollegeRequest.Student(id: 1, name: "John"), date: Date())
let jsonEncoder = JSONEncoder()
let df = { let df = DateFormatter(); df.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZ"; return df }() as DateFormatter
jsonEncoder.dateEncodingStrategy = .formatted(df)
let a: Data? = try? jsonEncoder.encode(request)
I'd like to use a property list decoder to decode a binary plist of dictionaries
Object that makes dictionary:
struct ZipCode: Codable {
var zipCode: String
var city: String
let state: String
let latitude: String
let longitude: String
let timezone: String
let daylightSavingsFlag: String
let geopoint: String
enum CodingKeys: String, CodingKey {
case zipCode = "Zip"
case city = "City"
case state = "State"
case latitude = "Latitude"
case longitude = "Longitude"
case timezone = "Timezone"
case daylightSavingsFlag = "Daylight savings time flag"
case geopoint = "geopoint"
}
}
Wrapper object:
struct ZipCodeList: Codable {
var zipCodes: [String:ZipCode]
}
Me trying to read it in which results in nil zipCodelist:
do {
let path = Bundle.main.path(forResource: "ZipCodes", ofType: "plist")
let binary = FileManager.default.contents(atPath: path!)
let zipCodes = try? PropertyListDecoder().decode(ZipCodeList.self, from: binary!)
print("Hi")
} catch {
}
Your plist doesn’t have an element zipCodes as a root element, instead decode as
let zipCodes = try? PropertyListDecoder().decode([String: ZipCode].self, from: binary!)
New on here and to Swift so please go easy on me..
Am a bit stuck when trying to parse JSON which contains nested dictionaries. I imagine its something wrong with the data strutures I have created and I have tryed everthing to rectify but still getting the same issue.
This is the JSON api I am trying to work with:
https://api.coindesk.com/v1/bpi/currentprice.json
These are the data structures I have created to model this:
struct base: Decodable {
let disclaimer: String
let bpi: [Bpi]
}
struct Bpi: Decodable {
let USD: [USD]
}
struct USD: Decodable {
let rate_float: Float
}
And here is my code in the VC :
override func viewDidLoad() {
super.viewDidLoad()
let jsonURLString = "https://api.coindesk.com/v1/bpi/currentprice.json"
guard let url = URL(string: jsonURLString) else {return}
URLSession.shared.dataTask(with: url) { (data, response, err) in
guard let data = data else {return}
do {
let bitcoinData = try JSONDecoder().decode(base.self, from: data)
print(bitcoinData.bpi)
} catch {
print("error")
}
} .resume() // Fires off the session
}
I can grab the data from the disclaimer string or the other strings in the root dictionary but that is it. I cannot parse anything further with the nested dictonaries - it just throws back the catch error.
Here is the JSON:
{
"time": {
"updated": "Nov 2, 2017 06:08:00 UTC",
"updatedISO": "2017-11-02T06:08:00+00:00",
"updateduk": "Nov 2, 2017 at 06:08 GMT"
},
"disclaimer": "This data was produced from the CoinDesk Bitcoin Price Index (USD). Non-USD currency data converted using hourly conversion rate from openexchangerates.org",
"chartName": "Bitcoin",
"bpi": {
"USD": {
"code": "USD",
"symbol": "$",
"rate": "6,889.4013",
"description": "United States Dollar",
"rate_float": 6889.4013
},
"GBP": {
"code": "GBP",
"symbol": "£",
"rate": "5,184.4053",
"description": "British Pound Sterling",
"rate_float": 5184.4053
},
"EUR": {
"code": "EUR",
"symbol": "€",
"rate": "5,910.4587",
"description": "Euro",
"rate_float": 5910.4587
}
}
}
Is there something I am clearly doing wrong here?
Thanks for the help in advance and sorry if my formatting sucks!
Try following model, with this it works - both bpi and USD are not arrays, just single values:
struct base: Decodable {
let disclaimer: String
let bpi: Bpi
}
struct Bpi: Decodable {
let USD: USD
}
struct USD: Decodable {
let rate_float: Float
}
Dictionaries (Dictionary<K,V>) are implicitly Decodable compliant if both generic types K and V are decodable.
Assuming you create a struct Coin for the currencies
struct Coin: Decodable {
private enum CodingKeys : String, CodingKey {
case code, symbol, rate, description, rateFloat = "rate_float"
}
let code : String
let symbol : String
let rate : String
let description : String
let rateFloat : Float
}
you can easily decode the currency dictionaries as [String:Coin] without any additional code
struct Base: Decodable {
private enum CodingKeys : String, CodingKey {
case disclaimer, coins = "bpi"
}
let disclaimer: String
let coins: [String:Coin]
}
And use it
let bitcoinData = try JSONDecoder().decode(Base.self, from: data)
print(bitcoinData.coins)
Alternatively if you want the currencies as an array of Coin you can write a custom initializer and map the dictionary values to an array.
This example decodes also the updatedISO value in the time dictionary
struct Base: Decodable {
struct Time : Decodable {
private enum CodingKeys : String, CodingKey {
case updated = "updatedISO"
}
let updated : Date
}
private enum CodingKeys : String, CodingKey {
case disclaimer, bpi, time
}
let disclaimer: String
let coins: [Coin]
let updated : Date
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
disclaimer = try container.decode(String.self, forKey: .disclaimer)
let bpi = try container.decode([String:Coin].self, forKey: .bpi)
coins = Array(bpi.values.sorted(by: {$0.code < $1.code}))
let time = try container.decode(Time.self, forKey: .time)
updated = time.updated
}
}
And use this example
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
let bitcoinData = try decoder.decode(Base.self, from: data)
print(bitcoinData.coins)
You declared your bpi & use properties as arrays but they were dictionaries (nested json objects). If you have sample JSON you can try this converter next time: https://danieltmbr.github.io/JsonCodeGenerator/
It generates the following output:
struct Root: Codable {
let time: Time
let disclaimer: String
let chartName: String
let bpi: Bpi
}
struct Time: Codable {
let updated: String
let updatedISO: String
let updateduk: String
}
struct Bpi: Codable {
let USD: USD
let GBP: USD
let EUR: USD
}
struct USD: Codable {
let code: String
let symbol: String
let rate: String
let description: String
let rateFloat: Double
private enum CodingKeys: String, CodingKey {
case code
case symbol
case rate
case description
case rateFloat = "rate_float"
}
}