In decoding JSON with Swift 4, I would like to convert a string during decoding into capital case. The JSON stores it as uppercase
For example
let title = "I CANT STAND THE RAIN"
print(title.capitalized)
How can I do this during the decoding process so the string is stored as capitalized in my model?
The only caveat is that I only want to capitalize one of the properties in the JSON (title) not the rest of them.
struct Book: Decodable {
let title: String
let author: String
let genre: String
init(newTitle: String, newAuthor: String, newGenre: String) {
title = newTitle
author = newAuthor
genre = newGenre
}
}
let book = try! decoder.decode(Book.self, from: jsonData)
You can provide your own custom Decodable initializer for your struct.
struct Book: Decodable {
let title: String
let author: String
let genre: String
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
title = try values.decode(String.self, forKey: .title).capitalized
author = try values.decode(String.self, forKey: .author)
genre = try values.decode(String.self, forKey: .genre)
}
enum CodingKeys: String, CodingKey {
case title, author, genre
}
}
jsonString.replace(/"\s*:\s*"[^"]/g, match => {
return match.slice(0, -1) + match[match.length - 1].toUpperCase()
})
Related
This question already has answers here:
What Is Preventing My Conversion From String to Int When Decoding Using Swift 4’s Codable?
(2 answers)
Closed 2 years ago.
I have understand how to make the codable wrapper class for services response structure.
But some times in server side the attribute value varies It may Int Or String.
Example
struct ResponseDataModel : Codable{
let data : DataClass?
enum CodingKey: String, CodingKey{
case data = "data"
}
init(from decoder: Decoder) throw {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass.self, forKey:.data)
}
}
struct DataClass : Codable{
let id : Int
let name : String?
let age : Int?
enum CodingKey: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"
}
init(from decoder: Decoder) throw {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(Int.self, forKey:.it)
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}
I would like to use generic way if id int string no matter what its it should bind to my controller with id value data.
let id : <T>
How to write the codable in generic formate.
You can do that using the following model:
struct ResponseDataModel<T: Codable>: Codable{
let data : DataClass<T>?
enum CodingKeys: String, CodingKey{
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass<T>.self, forKey:.data)
}
}
struct DataClass<T: Codable>: Codable {
let id: T?
let name: String?
let age: Int?
enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(T.self, forKey:.id)
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}
However, you should always known the type of the id property when you call decode(_:from:) function of JSONDecoder like this:
let decoder = JSONDecoder()
do {
let decoded = try decoder.decode(ResponseDataModel<Int>.self, from: data)
print(decoded)
} catch {
print(error)
}
Or you can use the following model to always map the id as Int, even if your server sends it as String:
struct ResponseDataModel: Codable{
let data : DataClass?
enum CodingKeys: String, CodingKey{
case data = "data"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
data = try values.decodeIfPresent(DataClass.self, forKey:.data)
}
}
struct DataClass: Codable {
let id: Int?
let name: String?
let age: Int?
enum CodingKeys: String, CodingKey{
case id = "id"
case name = "name"
case age = "age"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
do {
id = try values.decodeIfPresent(Int.self, forKey:.id)
} catch DecodingError.typeMismatch {
if let idString = try values.decodeIfPresent(String.self, forKey:.id) {
id = Int(idString)
} else {
id = nil
}
}
name = try values.decodeIfPresent(String.self, forKey:.name)
age = try values.decodeIfPresent(Int.self, forKey:.age)
}
}
First of all, here are some key points to take case when using Codable for parsing.
There is no need to every time implement enum CodingKeys if the property names and keys have exactly same name.
Also, no need to implement init(from:) if there no specific parsing requirements. Codable will handle all the parsing automatically if the models are written correctly as per the format.
So, with the above 2 improvements your ResponseDataModel looks like,
struct ResponseDataModel : Codable{
let data: DataClass?
}
Now, for DataClass you simply need to add an if-else condition to handle the Int and String cases. Implementing generics is not needed here.
Use String or Int as the type for id. And add the conditions accordingly. In the below code, I'm using id as String.
struct DataClass : Codable {
let id : String //here....
let name : String?
let age : Int?
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decodeIfPresent(String.self, forKey: .name)
age = try values.decodeIfPresent(Int.self, forKey: .age)
if let id = try? values.decode(Int.self, forKey: .id) {
self.id = String(id)
} else {
self.id = try values.decode(String.self, forKey:.id)
}
}
}
As per #Joakim Danielson example provided, you can reach desired result by attempting to decode value for each type.
struct Response: Decodable {
let id: String
let name: String?
let age: Int?
private enum CodingKeys: String, CodingKey {
case data
}
private enum NestedCodingKeys: String, CodingKey {
case id
case name
case age
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let nestedContainer = try container.nestedContainer(
keyedBy: NestedCodingKeys.self,
forKey: .data
)
if let id = try? nestedContainer.decode(Int.self, forKey: .id) {
self.id = String(id)
} else {
id = try nestedContainer.decode(String.self, forKey: .id)
}
name = try nestedContainer.decodeIfPresent(String.self, forKey: .name)
age = try nestedContainer.decodeIfPresent(Int.self, forKey: .age)
}
}
As #gcharita illustrated you can also catch DecodingError, but do-catch statement for decode(_:forKey:) would act only as an early exit, since it throws one of the following errors - typeMismatch, keyNotFound or valueNotFound for that particular key-value pair.
How can I decode the following JSON to a Codable object in Swift?
{"USD":"12.555", "EUR":"11.555"}
Here's the struct i'm using:
struct Prices: Codable {
var USD: Double
var EUR: Double
private enum CodingKeys: String, CodingKey {
case USD = "USD"
case EUR = "EUR"
}
init() {
self.USD = 0.0
self.EUR = 0.0
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.USD = try container.decode(Double.self, forKey: .USD)
self.EUR = try container.decode(Double.self, forKey: .EUR)
}
}
The error I'm getting is
Error typeMismatch(Swift.Double, Swift.DecodingError.Context(codingPath: [CodingKeys(stringValue: "USD", intValue: nil)], debugDescription: "Expected to decode Double but found a string/data instead.", underlyingError: nil))
I think your struct is incorrect and will be hard to maintain if you want to download more currency rates so I suggest a different approach. First create a struct for holding a currency rate
struct CurrencyRate {
let currency: String
let rate: Decimal?
}
Then decode the json as a dictionary and use map to convert it into an array of CurrencyRate
var rates = [CurrencyRate]()
do {
let result = try JSONDecoder().decode([String: String].self, from: json)
rates = result.map { CurrencyRate(currency: $0.key, rate: Decimal(string: $0.value))}
} catch {
print(error)
}
Two notes about CurrencyRate
You have two currencies in a rate so normally you also have another property named baseCurrency or otherCurrency or something similar but if that other currency always is the same you can omit it.
Depending on your use case it might also be a good idea to make a new type for currency properties, Currency
The values you're getting are Strings, so your struct should simply be
struct Prices: Codable {
let USD: String
let EUR: String
}
Depending you how you inted to use those price values, converting them to double may be inadvisable, because floating-point math is weird and Decimals may be better suited.
You have to decode the values as String and then cast it to Double. So, either Cast String to Double and provide a default value, like this:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.USD = Double(try container.decode(String.self, forKey: .USD)) ?? 0
self.EUR = Double(try container.decode(String.self, forKey: .EUR)) ?? 0
}
Or make the properties optional and remove the default values:
struct Prices: Codable {
var USD, EUR: Double?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.USD = Double(try container.decode(String.self, forKey: .USD))
self.EUR = Double(try container.decode(String.self, forKey: .EUR))
}
Note: Also, you don't need CodingKeys as the property names are the same as keys in the data.
Alternatively, you can use the computed property method, like this:
struct Prices: Codable {
var USDString: String
var EURString: String
private enum CodingKeys: String, CodingKey {
case USDString = "USD"
case EURString = "EUR"
}
var USD: Double? { Double(USDString) }
var EUR: Double? { Double(EURString) }
}
I'm trying to encode and decode data from an API that represent an object as an array of strings, for instance:
[
["username", "message", "date"],
["username", "message", "date"],
["username", "message", "date"]
]
This is the corresponding Codable struct:
struct Message: Codable {
let user: String
let content: String
let date: String
private enum CodingKeys: Int, CodingKey {
case user = 0
case content = 1
case date = 2
}
}
Neither encoding or decoding work; encoding shows that a JSON object is created instead of an array:
let msg = Message(user: "foo", content: "content", date: "2019-06-04")
let jsonData = try! JSONEncoder().encode(msg)
let jsonString = String(data: jsonData, encoding: .utf8)!
The final string is:
{"content":"content","user":"foo","date":"2019-06-04"}
My goal is to obtain the following string
["foo", "content", "2019-06-04"]
Using a custom encode/decode method in the struct solves this, but forces to create a lot of boilerplate for each struct/class.
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let values = try container.decode([String].self)
user = values[CodingKeys.user.rawValue]
content = values[CodingKeys.content.rawValue]
date = values[CodingKeys.date.rawValue]
}
How would one proceed to support this for any object?
And yes, this is a weird API, but this is not the first time I encounter one of those and user a different API format is not what I'm looking for here.
Rather than singleValueContainer use unkeyedContainer, it's more robust. If you want to assign the array items to struct members you have to write a custom initializer anyway
struct Message: Codable {
let user: String
let content: String
let date: String
init(from decoder: Decoder) throws {
var arrayContainer = try decoder.unkeyedContainer()
guard arrayContainer.count == 3 else { throw DecodingError.dataCorruptedError(in: arrayContainer, debugDescription: "The array must contain three items") }
user = try arrayContainer.decode(String.self)
content = try arrayContainer.decode(String.self)
date = try arrayContainer.decode(String.self)
}
func encode(to encoder: Encoder) throws {
var arrayContainer = encoder.unkeyedContainer()
try arrayContainer.encode(contentsOf: [user, content, date])
}
}
I am using Codable to try to Encode JSON to a Model but I get two errors.
Value of type 'KeyedEncodingContainer' has no member 'encoder'
Here's my code:
import UIKit
struct NewCustomer : Codable {
var firstName :String
var lastName :String
private enum CodingKeys : String, CodingKey {
case firstName
case lastName
}
func encode(to encoder :Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encoder(self.firstName, forKey: .firstName) // error here
try container.encoder(self.lastName, forKey: .lastName) // error here
}
}
let customer = NewCustomer(firstName: "Jake", lastName: "Reynolds")
let encodedCustomerJSON = try!
JSONEncoder().encode(customer)
print(encodedCustomerJSON)
print(String(data: encodedCustomerJSON, encoding: .utf8)!)
Change encoder to encode on the two lines giving errors. Please note that the line above (i.e. var container...) will keep encoder.
try container.encode(self.firstName, forKey: .firstName)
try container.encode(self.lastName, forKey: .lastName)
As already mentioned it's a typo encode vs. encoder:
try container.encode(...
Practically you don't need to specify CodingKeys and the encoding method at all in this case, this is sufficient:
struct NewCustomer : Codable {
var firstName, lastName : String
}
I am using Swift 4 and JSONDecoder. I have the following structure:
struct Customer: Codable {
var id: Int!
var cnum: String!
var cname: String!
}
Note: the fields cannot be made optional.
Now I have a JSON string:
[
{
"id": 1,
"cnum": "200",
"cname": "Bob Smith"
},
{
"id": 2,
"cnum": "201",
"cname": null
}
]
And to decode it, I use the following:
let decoder = JSONDecoder()
let customers = try decoder.decode([Customer].self, from: json)
Everything works fine except the null data gets converted to nil. My question is, what would be the easiest way to convert incoming nil to an empty string ("")?
I would like to do this with the minimum amount of code but I'm not sure about the correct approach and at what point can the nil be converted to an empty string. Thank you beforehand.
You can use backing ivars:
struct Customer: Codable {
var id: Int
var cnum: String {
get { _cnum ?? "" }
set { _cnum = newValue }
}
var cname: String {
get { _cname ?? "" }
set { _cname = newValue }
}
private var _cnum: String?
private var _cname: String?
private enum CodingKeys: String, CodingKey {
case id, _cnum = "cnum", _cname = "cname"
}
}
Due to the custom CodingKeys, the JSON decoder will actually decode to _cnum and _cname, which are optional strings. We convert nil to empty string in the property getters.
You can use decodeIfPresent method.
struct Source : Codable {
let id : String?
enum CodingKeys: String, CodingKey {
case id = "id"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
id = try values.decodeIfPresent(String.self, forKey: .id) ?? "Default value pass"
}
}
You should make a computed variable that will have the original value if it's not nil and an empty string if it is.
var cnameNotNil: String {
return cname ?? ""
}
The usual way is to write an initializer which handles the custom behavior. The null value is caught in an extra do - catch block.
struct Customer: Codable {
var id: Int
var cnum: String
var cname: String
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(Int.self, forKey: .id)
cnum = try container.decode(String.self, forKey: .cnum)
do { cname = try container.decode(String.self, forKey: .cname) }
catch { cname = "" }
}
}
Use decodeIfPresent if the value from response might be null
cnum = try container.decode(String.self, forKey: .cnum)
cname = try container.decodeIfPresent(String.self, forKey: .cname)