Make conforming struct to Codable (Encodable & Decodable) protocol is super easy: just declare it. But do I have to write all boiler code (CodingEnum, init(from decoder: Decoder), encode(to encoder: Encoder), etc) if I want make class conforming to Codable?
No, you haven't to do it. Example:
import Foundation
class Message: Codable {
let id: Int = 1
let text: String = "Hello"
}
let message = Message()
let encoder = JSONEncoder()
let json = try encoder.encode(message)
print(String(data: json, encoding: String.Encoding.utf8)!)
Related
Is there a generic way to serialize/deserialize objects for iOS? I was using the following code, and the system functions I was calling were deprecated in iOS 12:
func object(forKey:String) -> Any? {
if let data = get(BLOB_COL, forKey) {
return NSKeyedUnarchiver.unarchiveObject(with: data as! Data)
}
return nil
}
func set(_ object:Any, forKey: String) {
let data = NSKeyedArchiver.archivedData(withRootObject: object)
updateOrInsert(forKey, BLOB_COL, data)
}
It looks like the new versions of these functions requires knowledge of the object classes, and different signatures for unarchiving different collection types... is there a simple way to handle this generically?
If you would like to use NSKeyedArchiver/NSKeyedUnarchiver:
When archiving change:
NSKeyedArchiver.archivedData...
to
NSKeyedArchiver.archivedData(withRootObject: object, requiringSecureCoding: false)
And when unarchiving change
NSKeyedUnarchiver.unarchiveObject...
to
NSKeyedUnarchiver.unarchiveTopLevelObjectWithData(data)
Codable protocol generic approach
If you would like to use Codable to encode and decode (serialize) your Data you can extend Encodable protocol and create a method to encode any object that conforms to it:
extension Encodable {
func data(using encoder: JSONEncoder = JSONEncoder()) throws -> Data { try encoder.encode(self) }
}
And to decode the encoded data you can extend Data and create a generic method that decode any object that conforms to Decodable:
extension Data {
func object<T: Decodable>(using decoder: JSONDecoder = JSONDecoder()) throws -> T {
try decoder.decode(T.self, from: self)
}
// you can also create a string property to convert the JSON data to String
var string: String? { String(data: self, encoding: .utf8) }
}
Usage:
struct Person: Codable {
let name: String
let age: Int
}
do {
// encoding
let person = Person(name: "Joe", age: 10)
var people = [Person]()
people.append(person)
let data = try people.data()
print(data.string ?? "") // [{"name":"Joe","age":10}]
// decoding
let loadedPeople: [Person] = try data.object()
loadedPeople.forEach({print( $0.name, $0.age)}) // Joe 10
} catch {
print(error)
}
As Leo says, stop using NSKeyedArchiver and use the Codable protocol. That's the modern, Swifty way to serialize/deserialize your objects.
Before you answer:
I'm aware:
That an empty string is an invalid URL
That I could write a custom decoder for Employee
That I could declare url as a String
What I'm looking for is a better solution for decoding the optional URL itself. I'm hoping there's some Codable magic I'm missing!
So, I have JSON such as
let json = Data("""
{
"name": "Fred",
"url": ""
}
""".utf8)
and a corresponding object that contains an optional URL…
struct Employee: Decodable {
let name: String
let url: URL?
}
As url in the JSON is invalid, I'd like it to decode as nil, rather than throwing an error.
Trying the following doesn't work (it doesn't get called)…
extension Optional where Wrapped == URL {
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = try container.decode(URL.self)
} catch {
self = nil
}
}
}
In the past I've used…
struct FailableDecodable<T: Decodable>: Deodable {
let wrapped: T?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self.wrapped = try container.decode(T.self)
} catch {
print("Error decoding failable object: \(error)")
self.wrapped = nil
}
}
}
struct Employee: Decodable {
let name: String
let url: FailableDecodable<URL>?
}
but this requires me to continually refer to url.wrapped.
Is there a better solution?
If you are using Swift 5.1, you can use #propertyWrapper:
let json = """
{
"name": "Fred",
"url": ""
}
""".data(using: .utf8)!
#propertyWrapper
struct FailableDecodable<Wrapped: Decodable>: Decodable {
var wrappedValue: Wrapped?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try? container.decode(Wrapped.self)
}
}
struct Employee: Decodable {
let name: String
#FailableDecodable
private(set) var url: URL?
}
let employee = try! JSONDecoder().decode(Employee.self, from: json)
employee.url // nil
Edit — Codable version
If you need the top level struct to be Encodable as well you can use Codable conformance to the property wrapper.
#propertyWrapper
struct FailableDecodable<Wrapped: Codable>: Codable {
var wrappedValue: Wrapped?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try? container.decode(Wrapped.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
}
If url is nil this will output a JSON with url: null
{"name":"Fred","url":null}
If you wish to don't output the url property when nil you will need to implement a custom encoding (with encode(to:)) in Employee (which would mitigate the benefit of using a property wrapper).
Note: Using the default implementation of encode(to:) (not implementing it) works, but output an empty object when url is nil:
{"name":"Fred","url":{}}
If you get Return from initializer without initializing all stored properties warning for your Codable struct initializer that contains #FailableDecodable below will resolve it.
#propertyWrapper
struct FailableDecodable<Wrapped: Codable>: Codable {
var wrappedValue: Wrapped?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
wrappedValue = try? container.decode(Wrapped.self)
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
try container.encode(wrappedValue)
}
init() {
self.wrappedValue = nil
}
}
I am having a struct which conforms to the protocol Codable. I am having a property of type [String: Any]?. But the codable doesn't allow me to use it. Saying the error
does not conform to protocol 'Decodable
Use the old JSONSerialization class to convert between Data and [String: Any] if you must. Data is Codable. You could also use another format, just as String. Note that swift is strongly typed, so using an enum with associated values is generally preferred over Any. If the intent is to actually write to the sever and not to local storage then you can also consider just forgetting about Codable and using JSONSerialization for the whole thing.
Example:
import UIKit
import PlaygroundSupport
struct A: Codable {
let a: Int
let b: [String: Any]
enum CodingKeys: String, CodingKey {
case a
case b
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
a = try values.decode(Int.self, forKey: .a)
let data = try values.decode(Data.self, forKey: .b)
b = try JSONSerialization.jsonObject(with: data, options: []) as! [String: Any]
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(a, forKey: .a)
let data = try JSONSerialization.data(withJSONObject: b, options: [])
try container.encode(data, forKey: .b)
}
}
I want to encode Dictionary to json with JSONEncoder.
It seems like a Request, receive a dictionary as parameter and encode it to json as http body.
The code is looks like this:
let dict = ["name": "abcde"]
protocol Request {
var params: [String: Encodable] { get set }
func encode<T>(_ value: T) throws -> Data where T : Encodable
}
extension Request {
func encode<T>(_ value: T) throws -> Data where T : Encodable {
return try JSONEncoder().encode(value)
}
var body: Data? {
if let encoded = try? self.encode(self.params) {
return encoded
}
return nil
}
}
struct BaseRequest: Request {
var params: [String : Encodable]
}
let req = BaseRequest(params: dict)
let body = req.body
But this code occurs error
Fatal error: Dictionary<String, Encodable> does not conform to Encodable because Encodable does not conform to itself. You must use a concrete type to encode or decode.
How could I make this encodable?
You have to introduce type erasure as follows:
struct AnyEncodable: Encodable {
let value: Encodable
init(value: Encodable) {
self.value = value
}
func encode(to encoder: Encoder) throws {
try value.encode(to: encoder)
}
}
struct Model: Encodable {
var params: [String: AnyEncodable]
}
let encoder = JSONEncoder()
encoder.outputFormatting = .prettyPrinted
let json = try! encoder.encode(
Model(
params: [
"hello" : AnyEncodable.init(value: "world")
]
).params
)
print(String(data: json, encoding: .utf8))
If you want to define your struct as conforming to Codable, you can do it like this:
struct Model: Codable {
var param1: String
var param2: Int
}
let model = Model(param1: "test", param2: 0)
let encoded = try? JSONEncoder().encode(model)
let decoded = try? JSONDecoder().decode(Model.self, from: encoded!)
It won't really work if you set params: [String: Any] because the encoders/decoders don't know how to encode/decode Any, but they can do it for the primitive types.
If you want more help, you should read more about the new Codable protocol. I recommend this: https://hackernoon.com/everything-about-codable-in-swift-4-97d0e18a2999
I'm using the new Decodable protocol introduced in Swift 4.
Inside my unit test, I want to use a generic method that decodes a specific JSON file for a specific Decodable type.
I wrote the following function matching the JSONDecoder decode method:
var jsonDecoder: JSONDecoder = {
let decoder = JSONDecoder()
decoder.dateDecodingStrategy = .iso8601
return decoder
}()
static let bundle: Bundle = {
let testBundle = Bundle(for: Decodable.self)
let sampleURL = testBundle.url(forResource: "api_samples", withExtension: "bundle")!
return Bundle(url: sampleURL)!
}()
static func getJSONSample(fileName: String) throws -> Data {
let url = Decodable.bundle.url(forResource: fileName, withExtension: "json")!
return try Data(contentsOf: url)
}
func assertDecode<Obj>(_ type: Obj.Type, fileName: String) where Obj: Decodable {
do {
let data = try Decodable.getJSONSample(fileName: fileName)
let _ = try jsonDecoder.decode(type, from: data)
// Same by using Obj.self, Obj.Type
} catch let error {
XCTFail("Should not have failed for \(type) with json \(fileName): \(error)")
}
}
The compiler gives me the following error:
In argument type 'Obj.Type', 'Obj' does not conform to expected type 'Decodable'
I would have imagine that Obj is decodable due to the where clause.
What is wrong with that function?
Instead of doing a "where" statement, make your life easier by restricting the generic itself:
func assertDecode<Obj: Decodable>(_ type: Obj.Type, fileName: String)