Using Codable to Encode/Decode from Strings to Ints with a Function in Between

Using Codable to encode/decode from Strings to Ints with a function in between

For Codable encoding and decoding that requires custom transformation type stuff, like that, you just need to implement the initializer and encode methods yourself. In your case it would look something like this. It's a bit verbose just to try to get the idea across really clearly.

init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
let hexAString: String = try container.decode(String.self, forKey: .hexA)
self.hexA = hexAConversion(from: hexAString)
let hexBString: String = try container.decode(String.self, forKey: .hexB)
self.hexB = hexBConversion(from: hexBString)
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
//Assuming you have another set of methods for converting back to a String for encoding
try container.encode(self.name, forKey: .name)
try container.encode(hexAStringConversion(from: self.hexA), forKey: .hexA)
try container.encode(hexBStringConversion(from: self.hexB), forKey: .hexB)
}

Using codable with value that is sometimes an Int and other times a String

struct GeneralProduct: Codable {
var price: Double?
var id: String?
var name: String?
private enum CodingKeys: String, CodingKey {
case price = "p", id = "i", name = "n"
}
init(price: Double? = nil, id: String? = nil, name: String? = nil) {
self.price = price
self.id = id
self.name = name
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
price = try container.decode(Double.self, forKey: .price)
name = try container.decode(String.self, forKey: .name)
do {
id = try String(container.decode(Int.self, forKey: .id))
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}
}
}


let json1 = """
{
"p":2.12,
"i":"3k3mkfnk3",
"n":"Blue Shirt"
}
"""

let json2 = """
{
"p":2.12,
"i":0,
"n":"Blue Shirt"
}
"""


do {
let product = try JSONDecoder().decode(GeneralProduct.self, from: Data(json2.utf8))
print(product.price ?? "nil")
print(product.id ?? "nil")
print(product.name ?? "nil")
} catch {
print(error)
}

edit/update:

You can also simply assign nil to your id when your api returns 0:

do {
let value = try container.decode(Int.self, forKey: .id)
id = value == 0 ? nil : String(value)
} catch DecodingError.typeMismatch {
id = try container.decode(String.self, forKey: .id)
}

Encodable: Flattening structures

But how would encoding work in this instance?

It just works:

struct SomeObject: Codable { 
let id: Int
let details: SomeDetails
}
struct SomeDetails: Codable {
let count: Int
let name: String
let type: String
}
let someobject = SomeObject(id: 10, details: SomeDetails(count: 3, name: "ho", type: "hey"))
let json = try! JSONEncoder().encode(someobject)

If you insist on flattening it artificially, simply write your own encode(to:), like this:

struct SomeObject: Codable {
let id: Int
let details: SomeDetails
enum Keys : String, CodingKey {
case id
case count
case name
case type
}
func encode(to enc: Encoder) throws {
var con = try enc.container(keyedBy: Keys.self)
try con.encode(id, forKey: .id)
try con.encode(details.count, forKey: .count)
try con.encode(details.name, forKey: .name)
try con.encode(details.type, forKey: .type)
}
}
struct SomeDetails: Codable {
let count: Int
let name: String
let type: String
}
let someobject = SomeObject(id: 10, details: SomeDetails(count: 3, name: "ho", type: "hey"))
let json = try! JSONEncoder().encode(someobject)

Using JSONEncoder to encode a variable with Codable as type

Use a generic type constrained to Encodable

func saveObject<T : Encodable>(_ object: T, at location: String) {
//Some code

let data = try JSONEncoder().encode(object)

//Some more code
}

Swift 4 Codable decode URL from String

You can store URLs in Property Lists, and use the default Decodable implementation.

Based on the implementation of the Decodable protocol init in the Swift standard library, you must store the URL as a dictionary in the Plist, in a key called "relative". Optionally, you can also include a nested URL dictionary named "base" in the dictionary. These two values will get passed to the URL(string: relative, relativeTo: base) constructor during decoding.

For example:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<array>
<dict>
<key>apiHost</key>
<dict>
<key>relative</key>
<string>https://example.com/api/v0</string>
</dict>
<key>otherKey</key>
<string>SomeValue</string>
</dict>
</array>
</plist>

This Plist will decode using the following:

struct PlistItem: Codable {
let apiHost: URL
let otherKey: String
}
guard let filePath = Bundle.main.url(forResource: "Data", withExtension: "plist") else { return }
do {
let data = try Data(contentsOf: filePath)
let decoder = PropertyListDecoder()
let values = try decoder.decode([PlistItem].self, data)
}
catch { }

Dealing with dynamic value types with Swift and Codeable

You can use an enum for that. So it could be either a String or an Int or Unknown:

extension ContentItem {
enum SeriesId: Codable {
case text(String)
case number(Int)

case unknown
}
}

Then you can implement as a Decodable:

extension ContentItem.SeriesId {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let text = try? container.decode(String.self) {
self = .text(text)
} else if let number = try? container.decode(Int.self) {
self = .number(number)
} else {
assertionFailure("Unknown SeriesId type")
self = .unknown
}
}
}

Note that you can expand this to manage whatever Empty means (Since it could be a number as well and empty number is unknown).

Also you can expand it to make it encodable too:

extension ContentItem.SeriesId {
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .text(let text): try container.encode(text)
case .number(let number): try container.encode(number)
case .unknown: throw NSError(domain: "Unknown case is not encodable", code: -1, userInfo: nil)
}
}
}

So you only need to change the type of the series_id:

struct ContentItem: Codable {
let content_id: String?
let series_id: SeriesId?
let rank: Int
let score: Float
}

JSONEncoder won't allow type encoded to primitive value

There is a bug report for this:

https://bugs.swift.org/browse/SR-6163

SR-6163: JSONDecoder cannot decode RFC 7159 JSON

Basically, since RFC-7159, a value like 123 is valid JSON, but JSONDecoder won't support it. You may follow up on the bug report to see any future fixes on this. [The bug was fixed starting in iOS 13.]

#Where it fails#

It fails in the following line of code, where you can see that if the object is not an array nor dictionary, it will fail:

https://github.com/apple/swift-corelibs-foundation/blob/master/Foundation/JSONSerialization.swift#L120

open class JSONSerialization : NSObject {
//...

// top level object must be an Swift.Array or Swift.Dictionary
guard obj is [Any?] || obj is [String: Any?] else {
return false
}

//...
}

#Workaround#

You may use JSONSerialization, with the option: .allowFragments:

let jsonText = "123"
let data = Data(jsonText.utf8)

do {
let myString = try JSONSerialization.jsonObject(with: data, options: .allowFragments)
print(myString)
}
catch {
print(error)
}

Encoding to key-value pairs

Finally, you could also have your JSON objects look like this:

{ "integer": 123456 }

or

{ "string": "potatoe" }

For this, you would need to do something like this:

import Foundation 

enum MyValue {
case integer(Int)
case string(String)
}

extension MyValue: Codable {

enum CodingError: Error {
case decoding(String)
}

enum CodableKeys: String, CodingKey {
case integer
case string
}

init(from decoder: Decoder) throws {

let values = try decoder.container(keyedBy: CodableKeys.self)

if let integer = try? values.decode(Int.self, forKey: .integer) {
self = .integer(integer)
return
}

if let string = try? values.decode(String.self, forKey: .string) {
self = .string(string)
return
}

throw CodingError.decoding("Decoding Failed")
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodableKeys.self)

switch self {
case let .integer(i):
try container.encode(i, forKey: .integer)
case let .string(s):
try container.encode(s, forKey: .string)
}
}

}

let theEncodedValue = try! JSONEncoder().encode(MyValue.integer(123456))
let theEncodedString = String(data: theEncodedValue, encoding: .utf8)
print(theEncodedString!) // { "integer": 123456 }
let theDecodedValue = try! JSONDecoder().decode(MyValue.self, from: theEncodedValue)

Swift 4 JSON Decodable simplest way to decode type change

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
}
}


Related Topics



Leave a reply



Submit