Decode a nested object in Swift 5 with custom initializer
This can’t work for multiple reasons. You are trying to decode an array, so your custom decoding implementation from DailyCount won’t be called at all (if it were to compile) since at the top level your JSON contains an object, not an array.
But there is a much simpler solution which doesn’t even require implementing Decodable yourself.
You can create a generic wrapper struct for your outer object and use that with whatever payload type you need:
struct Wrapper<Payload: Decodable>: Decodable {
var data: Payload
}
You then can use this to decode your array of DailyCount structs:
let reports = try JSONDecoder().decode(Wrapper<[DailyCount]>.self, from: data).data
This can be made even more transparent by creating an extension on JSONDecoder:
extension JSONDecoder {
func decode<T: Decodable>(payload: T.Type, from data: Data) throws -> T {
try decode(Wrapper<T>.self, from: data).data
}
}
Is it possible to pass multiple data types inside a JsonDecoder?
Make the protocol conform to Decodable
and use a generic function
protocol CommonStruct: Decodable {}
func decode<T: CommonStruct>(from data: Data) throws -> T {
try JSONDecoder().decode(T.self, from: data)
}
Or if you have no other use for your protocol then you can skip it and do
func decode<T: Decodable>(from data: Data) throws -> T {
try JSONDecoder().decode(T.self, from: data)
}
Create a generic Data initializer for Decodable types using Swift
You can't overwrite class itself, but you can init it, init object from json and then assign values/ If take your code - it'll be something like this:
public class MyResponse: Codable {
public var id: String?
public var context: String?
public var results: [MyResult]?
}
public extension MyResponse {
convenience init(data: Data) throws {
self.init()
let object = try JSONDecoder().decode(MyResponse.self, from: data)
self.id = object.id
self.context = object.context
self.results = object.results
}
}
If you really don't need a class it's better to use struct instead of it, and it can be like this:
public struct MyResponse: Codable {
public let id: String?
public let context: String?
public let results: [String]?
}
public extension MyResponse {
init(data: Data) throws {
self = try JSONDecoder().decode(MyResponse.self, from: data)
}
}
Swift custom decodable initializer without CodingKeys
The auto-generation for CodingKeys
is really weird. The scope and availability of it changes based on what members you have.
Say you just have a Decodable
. These compile:
struct Decodable: Swift.Decodable {
static var codingKeysType: CodingKeys.Type { CodingKeys.self }
}
struct Decodable: Swift.Decodable {
static func `init`(from decoder: Decoder) throws -> Self {
_ = CodingKeys.self
return self.init()
}
}
…and you can put them together, if you add private
.
struct Decodable: Swift.Decodable {
private static var codingKeysType: CodingKeys.Type { CodingKeys.self }
static func `init`(from decoder: Decoder) throws -> Self {
_ = CodingKeys.self
return self.init()
}
}
…But make that func
an initializer, and again, no compilation.
struct Decodable: Swift.Decodable {
private static var codingKeysType: CodingKeys.Type { CodingKeys.self }
init(from decoder: Decoder) throws {
_ = CodingKeys.self
}
}
You can change it to be fully Codable
, not just Decodable
…
struct Decodable: Codable {
private static var codingKeysType: CodingKeys.Type { CodingKeys.self }
init(from decoder: Decoder) throws {
_ = CodingKeys.self
}
}
But then you can't use CodingKeys
at type scope, so the property won't compile.
Considering you probably don't need such a property, just use Codable
, file a bug with Apple referencing this answer, and hopefully we can all switch to Decodable
when they fix it. /p>
Use `self =` in convenience initializers to delegate to JSONDecoder or factory methods in Swift to avoid `Cannot assign to value: 'self' is immutable`
First, note that this limitation exists only for class
es, so the example initializer will work for as-is for struct
s and enum
s, but not all situations allow changing a class
to one of these types.
This limitation on class
initializers is a frequent pain-point that shows up often on this site (for example). There is a thread on the Swift forums discussing this issue, and work has started to add the necessary language features to make the example code above compile, but this is not complete as of Swift 5.4.
From the thread:
Swift's own standard library and Foundation overlay hack around this missing functionality by making classes conform to dummy protocols and using protocol extension initializers where necessary to implement this functionality.
Using this idea to fix the example code yields
final class Test: Codable {
let foo: Int
init(foo: Int) {
self.foo = foo
}
func jsonData() throws -> Data {
try JSONEncoder().encode(self)
}
}
protocol TestProtocol: Decodable {}
extension Test: TestProtocol {}
extension TestProtocol {
init(fromJSON data: Data) throws {
self = try JSONDecoder().decode(Self.self, from: data)
}
}
let test = Test(foo: 42)
let data = try test.jsonData()
let decodedTest = try Test(fromJSON: data)
print(decodedTest.foo)
which works fine. If Test
is the only type conforming to TestProtocol
, then only Test
will get this initializer.
An alternative is to simply extend Decodable
or another protocol to which your class conforms, but this may be undesirable if you do not want other types conforming to that protocol to also get your initializer.
multiple types in Codable
I definitely agree with @vadian. What you have is an optional rating. IMO this is a perfect scenario for using a propertyWrapper. This would allow you to use this Rated type with any model without having to manually implement a custom encoder/decoder to each model:
@propertyWrapper
struct RatedDouble: Codable {
var wrappedValue: Double?
init(wrappedValue: Double?) {
self.wrappedValue = wrappedValue
}
private struct Rated: Decodable {
let value: Double
}
public init(from decoder: Decoder) throws {
do {
wrappedValue = try decoder.singleValueContainer().decode(Rated.self).value
} catch DecodingError.typeMismatch {
let bool = try decoder.singleValueContainer().decode(Bool.self)
guard !bool else {
throw DecodingError.dataCorrupted(.init(codingPath: decoder.codingPath, debugDescription: "Corrupted data"))
}
wrappedValue = nil
}
}
public func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
guard let double = wrappedValue else {
try container.encode(false)
return
}
try container.encode(["value": double])
}
}
Usage:
struct AccountState: Codable {
let id: Int?
let favorite: Bool?
let watchlist: Bool?
@RatedDouble var rated: Double?
}
let json1 = #"{"id":550,"favorite":false,"rated":{"value":9.0},"watchlist":false}"#
let json2 = #"{"id":550,"favorite":false,"rated":false,"watchlist":false}"#
do {
let accountState1 = try JSONDecoder().decode(AccountState.self, from: Data(json1.utf8))
print(accountState1.rated ?? "nil") // "9.0\n"
let accountState2 = try JSONDecoder().decode(AccountState.self, from: Data(json2.utf8))
print(accountState2.rated ?? "nil") // "nil\n"
let encoded1 = try JSONEncoder().encode(accountState1)
print(String(data: encoded1, encoding: .utf8) ?? "nil")
let encoded2 = try JSONEncoder().encode(accountState2)
print(String(data: encoded2, encoding: .utf8) ?? "nil")
} catch {
print(error)
}
This would print:
9.0
nil
{"watchlist":false,"id":550,"favorite":false,"rated":{"value":9}}
{"watchlist":false,"id":550,"favorite":false,"rated":false}
Convert received Int to Bool decoding JSON using Codable
My suggestion: don't fight the JSON. Get it into a Swift value as quickly and with little fuss as possible, then do your manipulation there.
You can define a private internal structure to hold the decoded data, like this:
struct JSONModelSettings {
let patientID : String
let therapistID : String
var isEnabled : Bool
}
extension JSONModelSettings: Decodable {
// This struct stays very close to the JSON model, to the point
// of using snake_case for its properties. Since it's private,
// outside code cannot access it (and no need to either)
private struct JSONSettings: Decodable {
var patient_id: String
var therapist_id: String
var enabled: String
}
private enum CodingKeys: String, CodingKey {
case settings
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let settings = try container.decode(JSONSettings.self, forKey: .settings)
patientID = settings.patient_id
therapistID = settings.therapist_id
isEnabled = settings.enabled == "1"
}
}
Other JSON mapping frameworks, such as ObjectMapper allows you to attach a transform function to the encoding/decoding process. It looks like Codable
has no equivalence for now.
Related Topics
Cllocation Distancefromlocation (In Swift)
Best Way to Handle Errors from Async Closures in Swift 2
Multidimensional Dictionaries Possible in Swift
Alamofire 3 Custom Encoding to Alamofire 4 Custom Encoding
Swift Objc_Getassociatedobject Always Nil
How to Check Whether an Object Is Kind of a Dynamic Class Type in Swift
Swift: Hashable Struct with Dictionary Property
Implementing Reconnection with Urlsession Publisher and Combine
Scenekit Won't Scale a Dynamic Body
Safari App Extension Crashes After a Few Seconds for Hello World Project
Updating Fetchedresultscontroller for Predicate Set by Uisearchbar
How to Immediately See Swift Errors in Appcode
Swiftui Tabbar: Action for Tapping Tabitem of Currently Selected Tab to Reset View
Passing and Storing Closures/Callbacks in Swift
Swift Codable - Parse JSON Array Which Can Contain Different Data Type
Using Xcode to Cross-Compile Swift to Linux
Can Not Conform to Protocol by Creating Extension with Where Clauses