What parsing object when property can be integer or bool? - json

Sometimes server sends me property as bool (true, false).
Sometimes server sends me property as an integer (0,1).
How can I decodable this case via standard Decodable in Swift 4?
Example.
I have:
final class MyOffer : Codable {
var id = 0
var pickupAsap: Int?
enum CodingKeys: String, CodingKey {
case id
case pickupAsap = "pickup_asap"
}
}
Responses from server are:
1) "pickup_all_day": true,
2) "pickup_all_day": 0

you may implement your own decode init method, get each class property from decode container, during this section, make your logic dealing with wether "asap" is an Int or Bool, sign all required class properties at last.
here is a simple demo i made:
class Demo: Decodable {
var id = 0
var pickupAsap: Int?
enum CodingKeys: String, CodingKey {
case id
case pickupAsap = "pickup_asap"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let id = try container.decode(Int.self, forKey: .id)
let pickupAsapBool = try? container.decode(Bool.self, forKey: .pickupAsap)
let pickupAsapInt = try? container.decode(Int.self, forKey: .pickupAsap)
self.pickupAsap = pickupAsapInt ?? (pickupAsapBool! ? 1 : 0)
self.id = id
}
}
mock data:
let jsonInt = """
{"id": 10,
"pickup_asap": 0
}
""".data(using: .utf8)!
let jsonBool = """
{"id": 10,
"pickup_asap": true
}
""".data(using: .utf8)!
test:
let jsonDecoder = JSONDecoder()
let result = try! jsonDecoder.decode(Demo.self, from: jsonInt)
print("asap with Int: \(result.pickupAsap)")
let result2 = try! jsonDecoder.decode(Demo.self, from: jsonBool)
print("asap with Bool: \(result2.pickupAsap)")
output:
asap with Int: Optional(0)
asap with Bool: Optional(1)
for more info: Apple's encoding and decoding doc

Related

Parsing Dynamic JSON Model in Swift [duplicate]

This question already has answers here:
Swift JSOn Decoding Error: Expected to decode Array<Any> but found a dictionary instead
(2 answers)
Closed 1 year ago.
My app must parse JSON data. The software (IceStats) that generates the data, though, generates JSON with two slightly different structures. Sometimes, the "source" value is an array of dictionaries and sometimes it just one dictionary. I can parse the JSON when it is one way or the other, but I don't know how to handle it both ways.
Here is the JSON in the Array version:
{
"icestats": {
"admin": "dontcontactme#localhost",
"host": "server.badradio.biz",
"location": "Airport",
"server_id": "Icecast 2.4.4",
"server_start": "Mon, 26 Apr 2021 12:50:47 -0500",
"server_start_iso8601": "2021-04-26T12:50:47-0500",
"source": [
{
"audio_info": "bitrate=128",
"bitrate": 128,
"genre": "Automation",
"listener_peak": 7,
"listeners": 0,
"listenurl": "http://server.badradio.biz:8000/ambient",
"server_description": "No show is running, tune in for selections from the venerable tape series \"Comfortable & Economical\"",
"server_name": "Comfortable & Economical",
"server_type": "audio/mpeg",
"server_url": "badradio.biz",
"stream_start": "Fri, 30 Apr 2021 06:51:49 -0500",
"stream_start_iso8601": "2021-04-30T06:51:49-0500",
"title": "Vol-15-A",
"dummy": null
},
{
"listeners": 0,
"listenurl": "http://server.badradio.biz:8000/stream",
"dummy": null
}
]
}
}
And here it is in the Dictionary version:
{
"icestats": {
"admin": "dontcontactme#localhost",
"host": "server.badradio.biz",
"location": "Airport",
"server_id": "Icecast 2.4.4",
"server_start": "Mon, 26 Apr 2021 12:50:47 -0500",
"server_start_iso8601": "2021-04-26T12:50:47-0500",
"source": {
"audio_info": "bitrate=128",
"bitrate": 128,
"genre": "Automation",
"listener_peak": 2,
"listeners": 0,
"listenurl": "http://server.badradio.biz:8000/ambient",
"server_description": "No show is running, tune in for selections from the venerable tape series \"Comfortable & Economical\"",
"server_name": "Comfortable & Economical",
"server_type": "audio/mpeg",
"server_url": "badradio.biz",
"stream_start": "Wed, 28 Apr 2021 02:18:31 -0500",
"stream_start_iso8601": "2021-04-28T02:18:31-0500",
"title": "Vol-13-A",
"dummy": null
}
}
}
Finally, here is my data model that handles the Array version:
import Foundation
struct StreamData: Decodable {
let icestats: IceStats
}
struct IceStats: Decodable {
let source: [Source]
}
struct Source: Decodable {
let server_name: String?
let stream_start: String?
let title: String?
let server_description: String?
let server_url: String?
let genre: String?
}
Any help is greatly appreciated. Ideally, I could just change the format of the JSON, but I am not able to.
You can add some custom decoding logic to try both cases:
struct IceStats: Decodable {
var source: [Source]
enum CodingKeys : CodingKey {
case source
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let elements = try? container.decode([Source].self, forKey: .source) {
source = elements
} else if let element = try? container.decode(Source.self, forKey: .source) {
source = [element]
} else {
throw DecodingError.dataCorruptedError(forKey: .source, in: container, debugDescription: "Must be either a single or multiple sources!")
}
}
}
But this can get really long if you also want to decode other properties in IceStats, because you'll have to manually write the decoding code for those too. To avoid this, you can use a property wrapper:
#propertyWrapper
struct MultipleOrSingle<Element: Decodable>: Decodable {
let wrappedValue: [Element]
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let elements = try? container.decode([Element].self) {
wrappedValue = elements
} else if let element = try? container.decode(Element.self) {
wrappedValue = [element]
} else {
throw DecodingError.dataCorruptedError(in: container, debugDescription: "Container neither contains a single, nor multiple \(Element.self)!")
}
}
}
// Now you can just do this!
struct IceStats: Decodable {
#MultipleOrSingle
var source: [Source]
}
As per your array response your class looks like below and where you get response just decode that response class you will get all data
import Foundation
struct Response : Codable {
let icestats : Icestat?
enum CodingKeys: String, CodingKey {
case icestats = "icestats"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
icestats = Icestat(from: decoder)
}
}
struct Icestat : Codable {
let admin : String?
let host : String?
let location : String?
let serverId : String?
let serverStart : String?
let serverStartIso8601 : String?
let source : [Source]?
enum CodingKeys: String, CodingKey {
case admin = "admin"
case host = "host"
case location = "location"
case serverId = "server_id"
case serverStart = "server_start"
case serverStartIso8601 = "server_start_iso8601"
case source = "source"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
admin = try values.decodeIfPresent(String.self, forKey: .admin)
host = try values.decodeIfPresent(String.self, forKey: .host)
location = try values.decodeIfPresent(String.self, forKey: .location)
serverId = try values.decodeIfPresent(String.self, forKey: .serverId)
serverStart = try values.decodeIfPresent(String.self, forKey: .serverStart)
serverStartIso8601 = try values.decodeIfPresent(String.self, forKey: .serverStartIso8601)
source = try values.decodeIfPresent([Source].self, forKey: .source)
}
}
struct Source : Codable {
let audioInfo : String?
let bitrate : Int?
let dummy : AnyObject?
let genre : String?
let listenerPeak : Int?
let listeners : Int?
let listenurl : String?
let serverDescription : String?
let serverName : String?
let serverType : String?
let serverUrl : String?
let streamStart : String?
let streamStartIso8601 : String?
let title : String?
enum CodingKeys: String, CodingKey {
case audioInfo = "audio_info"
case bitrate = "bitrate"
case dummy = "dummy"
case genre = "genre"
case listenerPeak = "listener_peak"
case listeners = "listeners"
case listenurl = "listenurl"
case serverDescription = "server_description"
case serverName = "server_name"
case serverType = "server_type"
case serverUrl = "server_url"
case streamStart = "stream_start"
case streamStartIso8601 = "stream_start_iso8601"
case title = "title"
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
audioInfo = try values.decodeIfPresent(String.self, forKey: .audioInfo)
bitrate = try values.decodeIfPresent(Int.self, forKey: .bitrate)
dummy = try values.decodeIfPresent(AnyObject.self, forKey: .dummy)
genre = try values.decodeIfPresent(String.self, forKey: .genre)
listenerPeak = try values.decodeIfPresent(Int.self, forKey: .listenerPeak)
listeners = try values.decodeIfPresent(Int.self, forKey: .listeners)
listenurl = try values.decodeIfPresent(String.self, forKey: .listenurl)
serverDescription = try values.decodeIfPresent(String.self, forKey: .serverDescription)
serverName = try values.decodeIfPresent(String.self, forKey: .serverName)
serverType = try values.decodeIfPresent(String.self, forKey: .serverType)
serverUrl = try values.decodeIfPresent(String.self, forKey: .serverUrl)
streamStart = try values.decodeIfPresent(String.self, forKey: .streamStart)
streamStartIso8601 = try values.decodeIfPresent(String.self, forKey: .streamStartIso8601)
title = try values.decodeIfPresent(String.self, forKey: .title)
}
}
decode it like
let jsonData = jsonString.data(using: .utf8)!
let response = try! JSONDecoder().decode(Response.self, from: jsonData)
print(response.icestats)

ignore null object in array when parse with Codable swift

i'm parsing this API with swift Codable
"total": 7,
"searchResult": [
null,
{
"name": "joe"
"family": "adam"
},
null,
{
"name": "martin"
"family": "lavrix"
},
{
"name": "sarah"
"family": "mia"
},
null,
{
"name": "ali"
"family": "abraham"
}
]
with this PaginationModel:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: T?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
}
}
and User Model:
struct User: Codable {
var name: String?
var family: String?
}
i call jsonDecoder like this to parse API json:
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: json)
now my problem is null in searchResult Array. it parsed correctly and when i access to data in paginationModel i found null in array.
how can i ignore all null when parsing API, and result will be an array without any null
In the first place, I would advise to always consider PaginationModel to be composed from arrays. You don't have to pass [User] as the generic type, you can just pass User. Then the parser can use the knowledge that it parses arrays and handle null automatically:
class PaginationModel<T: Codable>: Codable {
var total: Int?
var data: [T]?
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = (try container.decodeIfPresent([T?].self, forKey: .data))?.compactMap { $0 }
}
}
You might want to remove optionals here and use some default values instead:
class PaginationModel<T: Codable>: Codable {
var total: Int = 0
var data: [T] = []
enum CodingKeys: String, CodingKey {
case total
case data = "searchResult"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = (try container.decodeIfPresent(Int.self, forKey: .total)) ?? 0
self.data = ((try container.decodeIfPresent([T?].self, forKey: .data)) ?? []).compactMap { $0 }
}
}
Simple solution, filter data after decoding
let responseObject = try JSONDecoder().decode(PaginationModel<[User?]>.self, from: data)
responseObject.data = responseObject.data?.filter{$0 != nil}
You may add an array type check within decode :
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.total = try container.decodeIfPresent(Int.self, forKey: .total)
self.data = try container.decodeIfPresent(T.self, forKey: .data)
//add the following:
if let array = self.data as? Array<Any?> {
self.data = ( array.compactMap{$0} as? T)
}
}
Note, you can just define the decodable variable that may be null/nil as [Float?] (or whatever type), with the optional '?' inside the array brackets.

Parsing JSON with wildcard keys

I'm parsing a poorly designed JSON structure in which I can expect to find values being reused as keys pointing to further data. Something like this
{"modificationDate" : "..."
"type" : "...",
"version" : 2,
"manufacturer": "<WILDCARD-ID>"
"<WILDCARD-ID>": { /* known structure */ } }
WILDCARD-ID can be just about anything at runtime, so I can't map it to a field in a struct somewhere at compile time. But once I dereference that field, its value has known structure, at which point I can follow the usual procedure for mapping JSON to structs.
I've found myself going down this path
let json = try JSONSerialization.jsonObject(with: data, options: .allowFragments) as? [String: Any]
let manDict = json["manufacturer"]
let data = NSKeyedArchiver.archivedData(withRootObject: manDict)
// now you have data!
but this seems very circuitous, which makes me think that maybe there's a cleaner way of accomplishing this?
You can use custom keys with Decodable, like so:
let json = """
{
"modificationDate" : "...",
"type" : "...",
"version" : 2,
"manufacturer": "<WILDCARD-ID>",
"<WILDCARD-ID>": {
"foo": 1
}
}
""".data(using: .utf8)!
struct InnerStruct: Decodable { // just as an example
let foo: Int
}
struct Example: Decodable {
let modificationDate: String
let type: String
let version: Int
let manufacturer: String
let innerData: [String: InnerStruct]
enum CodingKeys: String, CodingKey {
case modificationDate, type, version, manufacturer
}
struct CustomKey: CodingKey {
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.stringValue = "\(intValue)";
self.intValue = intValue
}
}
init(from decoder: Decoder) throws {
// extract all known properties
let container = try decoder.container(keyedBy: CodingKeys.self)
self.modificationDate = try container.decode(String.self, forKey: .modificationDate)
self.type = try container.decode(String.self, forKey: .type)
self.version = try container.decode(Int.self, forKey: .version)
self.manufacturer = try container.decode(String.self, forKey: .manufacturer)
// get the inner structs with the unknown key(s)
var inner = [String: InnerStruct]()
let customContainer = try decoder.container(keyedBy: CustomKey.self)
for key in customContainer.allKeys {
if let innerValue = try? customContainer.decode(InnerStruct.self, forKey: key) {
inner[key.stringValue] = innerValue
}
}
self.innerData = inner
}
}
do {
let example = try JSONDecoder().decode(Example.self, from: json)
print(example)
}
You can capture the idea of "a specific, but currently unknown key" in a struct:
struct StringKey: CodingKey {
static let modificationDate = StringKey("modificationDate")
static let type = StringKey("type")
static let version = StringKey("version")
static let manufacturer = StringKey("manufacturer")
var stringValue: String
var intValue: Int?
init?(stringValue: String) { self.init(stringValue) }
init?(intValue: Int) { return nil }
init(_ stringValue: String) { self.stringValue = stringValue }
}
With that, decoding is straightforward, and only decodes the structure that matches the key:
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: StringKey.self)
modificationDate = try container.decode(String.self, forKey: .modificationDate)
type = try container.decode(String.self, forKey: .type)
version = try container.decode(Int.self, forKey: .version)
manufacturer = try container.decode(String.self, forKey: .manufacturer)
// Decode the specific key that was identified by `manufacturer`,
// and fail if it's missing
manufacturerData = try container.decode(ManufacturerData.self,
forKey: StringKey(manufacturer))
}

How to Add or Remove an object in result of JSON Codable Swift 4 (with dynamic Keys)

i want import JSON in swift with Codable, modify the object by adding or removing object, and export it in JSON.
Here, my structure
class GenericCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
required init?(stringValue: String) { self.stringValue = stringValue }
required init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
}
class ListSpecie : Codable {
var species: [String : Specie]
required init(from decoder: Decoder) throws
{
let container = try decoder.container(keyedBy: GenericCodingKeys.self)
self.species = [String: Specie]()
for key in container.allKeys{
let value = try container.decodeIfPresent(Specie.self, forKey: GenericCodingKeys(stringValue: key.stringValue)!)
self.species[key.stringValue] = value
}
}
}
class Specie : Codable {
var name : String?
var latinName : [String]?
enum CodingKeys: String, CodingKey {
case name = "l"
case latinName = "ll"
}
required init(from decoder: Decoder) throws
{
let sValues = try decoder.container(keyedBy: CodingKeys.self)
name = try sValues.decodeIfPresent(String.self, forKey: .name)
latinName = try sValues.decodeIfPresent(Array<String>.self, forKey: .latinName)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encodeIfPresent(name, forKey: .name)
try container.encodeIfPresent(latinName, forKey: .latinName)
}
}
Here, the is a code with sample JSON
let myJson = """
{
"especeID1": {
"l": "Ail",
"ll": ["Allium sativum L.","Allium"]
},
"especeID2": {
"l": "Artichaut",
"ll": ["Cynara cardunculus"]
}
}
"""
let jsonDATA = myJson.data(using: .utf8)!
do{
self.jsonResult = try JSONDecoder().decode(ListSpecie.self, from: jsonDATA)
}catch{
print(error.localizedDescription)
}
Here, I want append or remove specie Object on jsonResult
for myspecie in (self.jsonResult?.species)! {
print(myspecie.key + " " + myspecie.value.name!)
}
// Encodage
let encoder = JSONEncoder()
let productJSON = try! encoder.encode(self.jsonResult?.species)
let jsonString = String(data: productJSON, encoding: .utf8)!
Someone could tell me how i can append or remove a specie object in my jsonResult variable .
Thanks a lot for the help you can bring me.
First of all your code is too complicated, most of the code is redundant.
One class (consider a struct) is sufficient
class Specie : Codable {
var name : String?
var latinName : [String]?
enum CodingKeys: String, CodingKey {
case name = "l"
case latinName = "ll"
}
}
If name and latin name is supposed to appear everywhere declare the properties non-optional (remove the question marks).
And decode the JSON
self.jsonResult = try JSONDecoder().decode([String:Specie].self, from: jsonDATA)
jsonResult is now a dictionary ([String:Specie]), you can remove items
self.jsonResult.removeValue(forKey: "especeID2")
or add an item
let newSpecies = Specie()
newSpecies.name = "Potato"
newSpecies.latinName = ["Solanum tuberosum"]
self.jsonResult["especeID3"] = newSpecies
and encode the object
let encoder = JSONEncoder()
let productJSON = try! encoder.encode(self.jsonResult)
let jsonString = String(data: productJSON, encoding: .utf8)!

Swift 4 JSON Decodable simplest way to decode type change

With Swift 4's Codable protocol there's a great level of under the hood date and data conversion strategies.
Given the JSON:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
I want to coerce it into the following structure
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
The Date Decoding Strategy can convert a String based date into a Date.
Is there something that does that with a String based Float
Otherwise I've been stuck with using CodingKey to bring in a String and use a computing get:
enum CodingKeys: String, CodingKey {
case name, age
case sTaxRate = "tax_rate"
}
var sTaxRate: String
var taxRate: Float { return Float(sTaxRate) ?? 0.0 }
This sort of strands me doing more maintenance than it seems should be needed.
Is this the simplest manner or is there something similar to DateDecodingStrategy for other type conversions?
Update: I should note: I've also gone the route of overriding
init(from decoder:Decoder)
But that is in the opposite direction as it forces me to do it all for myself.
Using Swift 5.1, you may choose one of the three following ways in order to solve your problem.
#1. Using Decodable init(from:) initializer
Use this strategy when you need to convert from String to Float for a single struct, enum or class.
import Foundation
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: CodingKeys.name)
age = try container.decode(Int.self, forKey: CodingKeys.age)
let taxRateString = try container.decode(String.self, forKey: CodingKeys.taxRate)
guard let taxRateFloat = Float(taxRateString) else {
let context = DecodingError.Context(codingPath: container.codingPath + [CodingKeys.taxRate], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = taxRateFloat
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#2. Using an intermediate model
Use this strategy when you have many nested keys in your JSON or when you need to convert many keys (e.g. from String to Float) from your JSON.
import Foundation
fileprivate struct PrivateExampleJson: Decodable {
var name: String
var age: Int
var taxRate: String
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
}
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
init(from decoder: Decoder) throws {
let privateExampleJson = try PrivateExampleJson(from: decoder)
name = privateExampleJson.name
age = privateExampleJson.age
guard let convertedTaxRate = Float(privateExampleJson.taxRate) else {
let context = DecodingError.Context(codingPath: [], debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
taxRate = convertedTaxRate
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
#3. Using a KeyedDecodingContainer extension method
Use this strategy when converting from some JSON keys' types to your model's property types (e.g. String to Float) is a common pattern in your application.
import Foundation
extension KeyedDecodingContainer {
func decode(_ type: Float.Type, forKey key: Key) throws -> Float {
if let stringValue = try? self.decode(String.self, forKey: key) {
guard let floatValue = Float(stringValue) else {
let context = DecodingError.Context(codingPath: codingPath, debugDescription: "Could not parse json key to a Float object")
throw DecodingError.dataCorrupted(context)
}
return floatValue
} else {
let doubleValue = try self.decode(Double.self, forKey: key)
return Float(doubleValue)
}
}
}
struct ExampleJson: Decodable {
var name: String
var age: Int
var taxRate: Float
enum CodingKeys: String, CodingKey {
case name, age, taxRate = "tax_rate"
}
}
Usage:
import Foundation
let jsonString = """
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
"""
let data = jsonString.data(using: String.Encoding.utf8)!
let decoder = JSONDecoder()
let exampleJson = try! decoder.decode(ExampleJson.self, from: data)
dump(exampleJson)
/*
prints:
▿ __lldb_expr_126.ExampleJson
- name: "Bob"
- age: 25
- taxRate: 4.25
*/
Unfortunately, I don't believe such an option exists in the current JSONDecoder API. There only exists an option in order to convert exceptional floating-point values to and from a string representation.
Another possible solution to decoding manually is to define a Codable wrapper type for any LosslessStringConvertible that can encode to and decode from its String representation:
struct StringCodableMap<Decoded : LosslessStringConvertible> : Codable {
var decoded: Decoded
init(_ decoded: Decoded) {
self.decoded = decoded
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let decodedString = try container.decode(String.self)
guard let decoded = Decoded(decodedString) else {
throw DecodingError.dataCorruptedError(
in: container, debugDescription: """
The string \(decodedString) is not representable as a \(Decoded.self)
"""
)
}
self.decoded = decoded
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(decoded.description)
}
}
Then you can just have a property of this type and use the auto-generated Codable conformance:
struct Example : Codable {
var name: String
var age: Int
var taxRate: StringCodableMap<Float>
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
Although unfortunately, now you have to talk in terms of taxRate.decoded in order to interact with the Float value.
However you could always define a simple forwarding computed property in order to alleviate this:
struct Example : Codable {
var name: String
var age: Int
private var _taxRate: StringCodableMap<Float>
var taxRate: Float {
get { return _taxRate.decoded }
set { _taxRate.decoded = newValue }
}
private enum CodingKeys: String, CodingKey {
case name, age
case _taxRate = "tax_rate"
}
}
Although this still isn't as a slick as it really should be – hopefully a later version of the JSONDecoder API will include more custom decoding options, or else have the ability to express type conversions within the Codable API itself.
However one advantage of creating the wrapper type is that it can also be used in order to make manual decoding and encoding simpler. For example, with manual decoding:
struct Example : Decodable {
var name: String
var age: Int
var taxRate: Float
private enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.age = try container.decode(Int.self, forKey: .age)
self.taxRate = try container.decode(StringCodableMap<Float>.self,
forKey: .taxRate).decoded
}
}
You can always decode manually. So, given:
{
"name": "Bob",
"age": 25,
"tax_rate": "4.25"
}
You can do:
struct Example: Codable {
let name: String
let age: Int
let taxRate: Float
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
name = try values.decode(String.self, forKey: .name)
age = try values.decode(Int.self, forKey: .age)
guard let rate = try Float(values.decode(String.self, forKey: .taxRate)) else {
throw DecodingError.dataCorrupted(.init(codingPath: [CodingKeys.taxRate], debugDescription: "Expecting string representation of Float"))
}
taxRate = rate
}
enum CodingKeys: String, CodingKey {
case name, age
case taxRate = "tax_rate"
}
}
See Encode and Decode Manually in Encoding and Decoding Custom Types.
But I agree, that it seems like there should be a more elegant string conversion process equivalent to DateDecodingStrategy given how many JSON sources out there incorrectly return numeric values as strings.
I know that this is a really late answer, but I started working on Codable couple of days back only. And I bumped into a similar issue.
In order to convert the string to floating number, you can write an extension to KeyedDecodingContainer and call the method in the extension from init(from decoder: Decoder){}
For the problem mentioned in this issue, see the extension I wrote below;
extension KeyedDecodingContainer {
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
return nil
}
return Float(value)
}
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
guard let valueAsString = try? decode(transformFrom, forKey: key),
let value = Float(valueAsString) else {
throw DecodingError.typeMismatch(
type,
DecodingError.Context(
codingPath: codingPath,
debugDescription: "Decoding of \(type) from \(transformFrom) failed"
)
)
}
return value
}
}
You can call this method from init(from decoder: Decoder) method. See an example below;
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
taxRate = try container.decodeIfPresent(Float.self, forKey: .taxRate, transformFrom: String.self)
}
In fact, you can use this approach to convert any type of data to any other type. You can convert string to Date, string to bool, string to float, float to int etc.
Actually to convert a string to Date object, I will prefer this approach over JSONEncoder().dateEncodingStrategy because if you write it properly, you can include different date formats in the same response.
Hope I helped.
Updated the decode method to return non-optional on suggestion from #Neil.
I used Suran's version, but updated it to return non-optional value for decode(). To me this is the most elegant version. Swift 5.2.
extension KeyedDecodingContainer {
func decodeIfPresent(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float? {
guard let value = try decodeIfPresent(transformFrom, forKey: key) else {
return nil
}
return Float(value)
}
func decode(_ type: Float.Type, forKey key: K, transformFrom: String.Type) throws -> Float {
guard let str = try? decode(transformFrom, forKey: key),
let value = Float(str) else {
throw DecodingError.typeMismatch(Int.self, DecodingError.Context(codingPath: codingPath, debugDescription: "Decoding of \(type) from \(transformFrom) failed"))
}
return value
}
}
You can use lazy var to convert the property to another type:
struct ExampleJson: Decodable {
var name: String
var age: Int
lazy var taxRate: Float = {
Float(self.tax_rate)!
}()
private var tax_rate: String
}
One disadvantage of this approach is that you cannot define a let constant if you want to access taxRate, since the first time you access it, you are mutating the struct.
// Cannot use `let` here
var example = try! JSONDecoder().decode(ExampleJson.self, from: data)
The options above only deal with the situation that the given field is always String. Many times I've met APIs where the output was once a string, other times number. So this is my suggestion to solve this. It is up to you to alter this to throw exception or set the decoded value to nil.
var json = """
{
"title": "Apple",
"id": "20"
}
""";
var jsonWithInt = """
{
"title": "Apple",
"id": 20
}
""";
struct DecodableNumberFromStringToo<T: LosslessStringConvertible & Decodable & Numeric>: Decodable {
var value: T
init(from decoder: Decoder) {
print("Decoding")
if let container = try? decoder.singleValueContainer() {
if let val = try? container.decode(T.self) {
value = val
return
}
if let str = try? container.decode(String.self) {
value = T.init(str) ?? T.zero
return
}
}
value = T.zero
}
}
struct MyData: Decodable {
let title: String
let _id: DecodableNumberFromStringToo<Int>
enum CodingKeys: String, CodingKey {
case title, _id = "id"
}
var id: Int {
return _id.value
}
}
do {
let parsedJson = try JSONDecoder().decode(MyData.self, from: json.data(using: .utf8)!)
print(parsedJson.id)
} catch {
print(error as? DecodingError)
}
do {
let parsedJson = try JSONDecoder().decode(MyData.self, from: jsonWithInt.data(using: .utf8)!)
print(parsedJson.id)
} catch {
print(error as? DecodingError)
}
How to used JSONDecodable in Swift 4:
Get the JSON Response and Create Struct
Conform Decodable class in Struct
Other steps in this GitHub project, a simple example