Decoding a single var differently in a Decodable struct - json

I am trying to decode a single var that is sent as a hexadecimal string into a UInt32 by creating a custom init() in a Codable struct, but would like the remaining vars to be decoded automatically.
struct MyStruct : Decodable {
var bits : UInt32
var other1 : Double
... // there are 20 more keys
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
if let string = try? container.decode(String.self, forKey: .bits) {
self.bits = convertToUInt32(string)
}
// Is there a way to decode remaining keys automatically?
???
}
}
The JSON I get is:
{
"bits" : "1a2b3fdd",
"other" : 12.34,
...
}

You can create a property wrapper and provide a custom decoder for it. Then you can mark the properties that you want to be decoded:
#propertyWrapper
struct UInt32Hexa {
var wrappedValue: UInt32
}
extension UInt32Hexa: Decodable {
public init(from decoder: Decoder) throws {
let string = try decoder.singleValueContainer()
.decode(String.self)
guard let value = UInt32(string, radix: 16) else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Corrupted data: \(string)"))
}
self.wrappedValue = value
}
}
Playground testing:
let json = """
{
"bits" : "1a2b3fdd",
"other" : 12.34
}
"""
struct MyStruct : Decodable {
#UInt32Hexa var bits: UInt32
let other: Double
}
do {
let test = try JSONDecoder().decode(MyStruct.self, from: Data(json.utf8))
print(test.bits) // 439042013
} catch {
print(error)
}

Related

Swift 4 decodable with unknown dynamic keys

I have the following JSON
{"DynamicKey":6410,"Meta":{"name":"","page":""}}
DynamicKey is unknown at compile time.I'm trying to find a reference how to
parse this struct using decodable.
public struct MyStruct: Decodable {
public let unknown: Double
public let meta: [String: String]
private enum CodingKeys: String, CodingKey {
case meta = "Meta"
}
}
Any ideas?
To decode an arbitrary string, you need a key like this:
// Arbitrary key
private struct Key: CodingKey, Hashable, CustomStringConvertible {
static let meta = Key(stringValue: "Meta")!
var description: String {
return stringValue
}
var hashValue: Int { return stringValue.hash }
static func ==(lhs: Key, rhs: Key) -> Bool {
return lhs.stringValue == rhs.stringValue
}
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
This is a very general-purpose tool (expect for the static let meta) that can be used for all kinds of generic-key problems.
With that, you can find the first key that isn't .meta and use that as your dynamic key.
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
meta = try container.decode([String: String].self, forKey: .meta)
guard let dynamicKey = container.allKeys.first(where: { $0 != .meta }) else {
throw DecodingError.dataCorrupted(DecodingError.Context(codingPath: [],
debugDescription: "Could not find dynamic key"))
}
unknown = try container.decode(Double.self, forKey: dynamicKey)
}
All together as a playground:
import Foundation
let json = Data("""
{"DynamicKey":6410,"Meta":{"name":"","page":""}}
""".utf8)
public struct MyStruct: Decodable {
public let unknown: Double
public let meta: [String: String]
// Arbitrary key
private struct Key: CodingKey, Hashable, CustomStringConvertible {
static let meta = Key(stringValue: "Meta")!
var description: String {
return stringValue
}
var hashValue: Int { return stringValue.hash }
static func ==(lhs: Key, rhs: Key) -> Bool {
return lhs.stringValue == rhs.stringValue
}
let stringValue: String
init(_ string: String) { self.stringValue = string }
init?(stringValue: String) { self.init(stringValue) }
var intValue: Int? { return nil }
init?(intValue: Int) { return nil }
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Key.self)
meta = try container.decode([String: String].self, forKey: .meta)
guard let dynamicKey = container.allKeys.first(where: { $0 != .meta }) else {
throw DecodingError.dataCorrupted(.init(codingPath: [],
debugDescription: "Could not find dynamic key"))
}
unknown = try container.decode(Double.self, forKey: dynamicKey)
}
}
let myStruct = try! JSONDecoder().decode(MyStruct.self, from: json)
myStruct.unknown
myStruct.meta
This technique can be expanded to decode arbitrary JSON. Sometimes it's easier to do that, and then pull out the pieces you want, then to decode each piece. For example, with the JSON gist above, you could implement MyStruct this way:
public struct MyStruct: Decodable {
public let unknown: Double
public let meta: [String: String]
public init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
let json = try container.decode(JSON.self)
guard let meta = json["Meta"]?.dictionaryValue as? [String: String] else {
throw DecodingError.dataCorrupted(.init(codingPath: [],
debugDescription: "Could not find meta key"))
}
self.meta = meta
guard let (_, unknownJSON) = json.objectValue?.first(where: { (key, _) in key != "Meta" }),
let unknown = unknownJSON.doubleValue
else {
throw DecodingError.dataCorrupted(.init(codingPath: [],
debugDescription: "Could not find dynamic key"))
}
self.unknown = unknown
}
}
import UIKit
var str = """
{"DynamicKey":6410,"Meta":{"name":"","page":""}}
"""
public struct MyStruct: Decodable {
public var unknown: Double?
public var meta: [String: String]?
public init(from decoder: Decoder) {
guard let container = try? decoder.container(keyedBy: CodingKeys.self) else {
fatalError()
}
for key in container.allKeys {
unknown = try? container.decode(Double.self, forKey: key)//) ?? 0.0
if key.stringValue == "Meta" {
meta = try? container.decode([String: String].self, forKey: key)
}
}
print(container.allKeys)
}
struct CodingKeys: CodingKey {
var stringValue: String
init?(stringValue: String) {
self.stringValue = stringValue
}
var intValue: Int?
init?(intValue: Int) {
return nil
}
}
}
let jsonData = str.data(using: .utf8)!
let jsonDecoder = JSONDecoder()
let myStruct = try! jsonDecoder.decode(MyStruct.self, from: jsonData)
print("Meta : \(myStruct.meta)")
print("Double : \(myStruct.unknown)")
I've already answered a similar question
https://stackoverflow.com/a/48412139/1979882

Decodable and JSON, 2 datatypes for same variable

I'm using the Decodable protocol to decode some json, but I've run into a problem:
I'm getting an answer back, where a longitude and a latitide can be either an interger (latitude = 0) if there's no geo location data added to the element, and a String (fx. latitude = "25.047880") if there's geodata available. Now when I decode the json, I don't know how to build my Struct, as the long and lat can't both be String and Int.. So I'm getting a decode error when fetching elements where both cases are represented.
Any suggestions about how to solve this? I've tried with "Any" as datatype, but this doesn't conform to the Decodable protocol
struct JPhoto: Decodable {
let id: String
let farm: Int
let secret: String
let server: String
let owner: String
let title: String
let latitude: String //Can both be Int and String
let longitude: String //Can both be Int and String
}
You need to write your own encoder/decoder. You can use an associated value enum to do this, using a switch statement to encode and the throwing/catching behaviour to decode:
enum AngularDistance:Codable {
case string(String), integer(Int)
func encode(to encoder: Encoder) throws {
switch self {
case .string(let str):
var container = encoder.singleValueContainer()
try container.encode(str)
case .integer(let int):
var container = encoder.singleValueContainer()
try container.encode(int)
}
}
init(from decoder: Decoder) throws {
do {
let container = try decoder.singleValueContainer()
let str = try container.decode(String.self)
self = AngularDistance.string(str)
}
catch {
do { let container = try decoder.singleValueContainer()
let int = try container.decode(Int.self)
self = AngularDistance.integer(int)
}
catch {
throw DecodingError.typeMismatch(AngularDistance.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Expected to decode an Int or a String"))
}
}
}
}
Here's an example of encoding and decoding this AngularDistance type:
let lat = [AngularDistance.string("String"), AngularDistance.integer(10)]
let encoder = JSONEncoder()
var decoder = JSONDecoder()
do {
let encoded = try encoder.encode(lat)
try decoder.decode(Array<AngularDistance>.self, from: encoded)
}
catch DecodingError.typeMismatch(let t, let e) {
t
e.codingPath
e.debugDescription
}
catch {
print(error.localizedDescription)
}
And here's your struct rewritten:
struct JPhoto: Decodable {
let id: String
let farm: Int
let secret: String
let server: String
let owner: String
let title: String
let latitude: AngularDistance //Can both be Int and String
let longitude: AngularDistance //Can both be Int and String
}
There are couple of approaches to mention in addition to enums with associated value. You can use proposed Either<Int, String> or IntOrString struct for your latitude and longitude.
struct Either<F: Codable, S: Codable>: Codable {
var firstValue: F?
var secondValue: S?
var value: Any? {
return firstValue ?? secondValue
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if firstValue != nil {
try? container.encode(firstValue)
} else {
try? container.encode(secondValue)
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
firstValue = try? container.decode(F.self)
secondValue = try? container.decode(S.self)
if firstValue == nil && secondValue == nil {
//Type mismatch
throw DecodingError.typeMismatch(type(of: self), DecodingError.Context(codingPath: [], debugDescription: "The value is not of type \(F.self) and also not \(S.self)"))
}
}
}
Another way to do the same:
struct IntOrString: Codable {
var value: Any
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
if let intValue = value as? Int {
try? container.encode(intValue)
} else if let strValue = value as? String {
try? container.encode(strValue)
}
}
init(from decoder: Decoder) throws {
if let int = try? Int(from: decoder) {
value = int
return
}
value = try String(from: decoder)
}
}

Swift 4 JSON Parsing Int - what am I doing wrong?

Swift 4 JSON Parsing Int issue. All the examples I've seen Int is encoded / decoded out of the box. Anyone see what I am doing wrong?
Thanks!
JSON
let patientBundleEntry = """
{
"resourceType": "Bundle",
"id": "patientListBundle",
"type": "SearchSet",
"total": 123
}
"""
Classes
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int? // this is printing -> Optional(105553116787496) instead of 123
}
class Resource:Codable {
var resourceType:ResourceType? // this is printing fine
}
Test - my assert for total being 123 fails and the optional is a long number. Any ideas why? Is my encoding wrong using .utf8??
func testModelBundle(){
let jsonDataEncoded:Data? = patientBundleEntry.data(using: .utf8)!
guard let responseData = jsonDataEncoded else {
print("Error: did not receive data")
}
do {
let bundleDecoded = try JSONDecoder().decode(BundleFHIR.self, from: responseData)
print("bundleDecoded.resourceType resource type \(bundleDecoded.resourceType )") //THIS is right
print("bundleDecoded.resourceType total \(bundleDecoded.total )") THIS is wrong
assert(bundleDecoded.total == 123, "something is wrong") // ***** <- this assert fails and it prints Optional(105553116787496)
} catch {
print("error trying to convert data to JSON")
}
}
At first you have to decode and then parse the JSON data.
Follow the below code :
struct Patient: Codable {
var resourceType: String
var id: String
var type: String
var total: Int
}
let json = patientBundleEntry.data(using: .utf8)!
let decoder = JSONDecoder()
let patient = try! decoder.decode(Patient.self, from: json)
print(patient.total) // Prints - 123
Ok there were a lot of issues with my code. Mainly that I didn't have codingKeys as private and therefore tried to rename it because the inheritance tree could not discern between the two. This caused me to not be implementing the true protocol. Not sure why it was half working... but here is my final code and it works great!
class BundleFHIR: Resource {
var entry:[BundleEntry]?
var total:Int?
override init() {
super.init()
}
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
total = try values.decodeIfPresent(Int.self, forKey: .total)
entry = try values.decodeIfPresent([BundleEntry].self, forKey: .entry)
}
private enum CodingKeys: String, CodingKey
{
case total
case entry
}
}
class Resource:Codable {
var resourceType:ResourceType?
var id:String?
init(){
}
required init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
resourceType = try values.decode(ResourceType.self, forKey: .resourceType)
id = try values.decode(String.self, forKey: .id)
}
private enum CodingKeys: String, CodingKey
{
case resourceType
case id
}
}

Swift structures: handling multiple types for a single property

I am using Swift 4 and trying to parse some JSON data which apparently in some cases can have different type values for the same key, e.g.:
{
"type": 0.0
}
and
{
"type": "12.44591406"
}
I am actually stuck with defining my struct because I cannot figure out how to handle this case because
struct ItemRaw: Codable {
let parentType: String
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
throws "Expected to decode String but found a number instead.", and naturally,
struct ItemRaw: Codable {
let parentType: Float
enum CodingKeys: String, CodingKey {
case parentType = "type"
}
}
throws "Expected to decode Float but found a string/data instead." accordingly.
How can I handle this (and similar) cases when defining my struct?
I ran into the same issue when trying to decode/encode the "edited" field on a Reddit Listing JSON response. I created a struct that represents the dynamic type that could exist for the given key. The key can have either a boolean or an integer.
{ "edited": false }
{ "edited": 123456 }
If you only need to be able to decode, just implement init(from:). If you need to go both ways, you will need to implement encode(to:) function.
struct Edited: Codable {
let isEdited: Bool
let editedTime: Int
// Where we determine what type the value is
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Check for a boolean
do {
isEdited = try container.decode(Bool.self)
editedTime = 0
} catch {
// Check for an integer
editedTime = try container.decode(Int.self)
isEdited = true
}
}
// We need to go back to a dynamic type, so based on the data we have stored, encode to the proper type
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try isEdited ? container.encode(editedTime) : container.encode(false)
}
}
Inside my Codable class, I then use my struct.
struct Listing: Codable {
let edited: Edited
}
Edit: A more specific solution for your scenario
I recommend using the CodingKey protocol and an enum to store all the properties when decoding. When you create something that conforms to Codable the compiler will create a private enum CodingKeys for you. This lets you decide on what to do based on the JSON Object property key.
Just for example, this is the JSON I am decoding:
{"type": "1.234"}
{"type": 1.234}
If you want to cast from a String to a Double because you only want the double value, just decode the string and then create a double from it. (This is what Itai Ferber is doing, you would then have to decode all properties as well using try decoder.decode(type:forKey:))
struct JSONObjectCasted: Codable {
let type: Double?
init(from decoder: Decoder) throws {
// Decode all fields and store them
let container = try decoder.container(keyedBy: CodingKeys.self) // The compiler creates coding keys for each property, so as long as the keys are the same as the property names, we don't need to define our own enum.
// First check for a Double
do {
type = try container.decode(Double.self, forKey: .type)
} catch {
// The check for a String and then cast it, this will throw if decoding fails
if let typeValue = Double(try container.decode(String.self, forKey: .type)) {
type = typeValue
} else {
// You may want to throw here if you don't want to default the value(in the case that it you can't have an optional).
type = nil
}
}
// Perform other decoding for other properties.
}
}
If you need to store the type along with the value, you can use an enum that conforms to Codable instead of the struct. You could then just use a switch statement with the "type" property of JSONObjectCustomEnum and perform actions based upon the case.
struct JSONObjectCustomEnum: Codable {
let type: DynamicJSONProperty
}
// Where I can represent all the types that the JSON property can be.
enum DynamicJSONProperty: Codable {
case double(Double)
case string(String)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
// Decode the double
do {
let doubleVal = try container.decode(Double.self)
self = .double(doubleVal)
} catch DecodingError.typeMismatch {
// Decode the string
let stringVal = try container.decode(String.self)
self = .string(stringVal)
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .double(let value):
try container.encode(value)
case .string(let value):
try container.encode(value)
}
}
}
One simple solution is to provide an implementation of init(from:) which attempts to decode the value as a String, and if that fails because the type is wrong, attempt to decode as a Double:
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
self.parentType = try container.decode(String.self, forKey: .parentType)
} catch DecodingError.typeMismatch {
let value = try container.decode(Double.self, forKey: .parentType)
self.parentType = "\(value)"
}
}
I had to decode PHP/MySQL/PDO double value that is given as an String, for this use-case I had to extend the KeyedDecodingContainer, like so:
extension KeyedDecodingContainer {
func decode(forKey key: KeyedDecodingContainer.Key) throws -> Double {
do {
let str = try self.decode(String.self, forKey: key)
if let dbl = Double(str) {
return dbl
}
} catch DecodingError.typeMismatch {
return try self.decode(Double.self, forKey: key)
}
let context = DecodingError.Context(codingPath: self.codingPath,
debugDescription: "Wrong Money Value")
throw DecodingError.typeMismatch(Double.self, context)
}
}
Usage:
let data = """
{"value":"1.2"}
""".data(using: .utf8)!
struct Test: Decodable {
let value: Double
enum CodingKeys: String, CodingKey {
case value
}
init(from decoder: Decoder) throws {
self.value = try decoder.container(keyedBy: CodingKeys.self)
.decode(forKey: CodingKeys.value)
}
}
try JSONDecoder().decode(Test.self, from: data).value
// Out Put Json
{
"software_id": "10",
"name": "Kroll"
},
{
"software_id": 580,
"name": "Synmed"
}
// Codable Struct
struct SoftwareDataModel: Codable {
var softwareId:MyValue?
var name:String?
enum CodingKeys: String, CodingKey{
case softwareId = "software_id"
case name
}
}
MYValue is Codable Struct Which help to to convert your datatype into "String" here I mentions only String and Int datatypes.
enum MyValue: Codable {
case string(String)
var stringValue: String? {
switch self {
case .string(let s):
return s
}
}
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode(String.self) {
self = .string(x)
return
}
if let x = try? container.decode(Int.self) {
self = .string("\(x)")
return
}
throw DecodingError.typeMismatch(MyValue.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for MyValue"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .string(let x):
try container.encode(x)
}
}
}
// How to get software_id ?
let softwareId = Struct_object.softwareId?.stringValue ?? "0"

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