Is Decodable inheritance even possible in Swift? - swift

I usually use structs that inherit from Decodable to serialize JSON data i'm pulling from my backend. However, I have an application where a JSON payload is identical to an existing struct with a few minor differences. Thus, I would like to have a decodable struct that inherits from another decodable struct like this:
class Party: Decodable {
var theme: String
}
class PublicParty : Party {
var address: String = ""
}
However, when I send the following JSON payload, only "theme" from the Party class gets decoded while the "address" from PublicParty remains "":
{
"theme": "Black out or get out",
"address": "404 Mundus, Tabasko Beach 45778 WA"
}
Swift code:
func test(completion: #escaping(Result<PublicParty, Error>) -> ()) {
guard let url = URL(string: "127.0.0.1:8000/test") else { return }
URLSession.shared.dataTask(with: url) { (data, response, error) in
// handle error
if let error = error {
completion(.failure(error))
return
}
// decode data
do {
let search = try JSONDecoder().decode(PublicParty.self, from: data!)
print(search)
completion(.success(search))
} catch let error {
completion(.failure(error))
}
}.resume()
}
As nothing is off in my front end/back end, does Swift even allow this functionality?

This worked:
class Party: Decodable {
var theme: String
}
class PublicParty : Party {
var address: String = ""
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let values = try decoder.container(keyedBy: CodingKeys.self)
address = try values.decode(String.self, forKey: .address)
}
private enum CodingKeys: String, CodingKey
{
case address
}
}

Related

How to handle two different JSON responses with Alamofire

I am making a login functionality in a SwiftUI app.
When the login in successful the JSON response is:
{
"user_id": 41,
"token": "Token",
"token_type": "bearer",
"expires_in": 12096000
}
When the login is failed the JSON response is:
{
"message": "this is a failure message"
}
I made two different structs to encode the responses
struct LoginResponseModelFailure:Codable {
let message:String
}
struct LoginResponseModelSuccess:Codable{
let user_id: Int
let token: String
let token_type : String
let expires_in: Int
}
Do I need to merge these two structs to single one? if so how to do that?
How can I handle two different responses using alamofire or urlSession?
To avoid optionals my suggestion is to declare the root object as enum with associated values
enum Response : Decodable {
case success(ResponseSuccess)
case failure(ResponseFailure)
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
do {
self = .success(try container.decode(ResponseSuccess.self))
} catch {
self = .failure(try container.decode(ResponseFailure.self))
}
}
}
The other structs can remain as they are except the names became camelCase
struct ResponseFailure : Decodable {
let message : String
}
struct ResponseSuccess : Decodable {
let userId : Int
let token : String
let tokenType : String
let expiresIn : Int
}
To decode the data switch on the result, data represents the received raw data. The key decoding strategy is added to handle the snake_case keys
do {
let decoder = JSONDecoder()
decoder.keyDecodingStrategy = .convertFromSnakeCase
let result = try decoder.decode(Response.self, from: data)
switch result {
case .success(let data): print(data)
case .failure(let error): print(error)
}
} catch {
print(error)
}
As mentioned in the comment, one way is to create one Struct but then you have to make all the properties Optional as below,
struct LoginResponse: Codable {
let user_id: Int?
let token: String?
let token_type : String?
let expires_in: Int?
let message: String?
}
So now you have to deal with all the optionals.
Another way that seems more appropriate is to introduce another Struct that holds success and failure but you have to implement the init(from decoder: Decoder) method as below,
struct LoginFailure: Codable {
let message:String
}
struct LoginSuccess: Codable {
let user_id: Int
let token: String
let token_type : String
let expires_in: Int
}
struct LoginRespone: Codable {
var data: LoginSuccess?
var message: LoginFailure?
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let data = try? container.decode(LoginSuccess.self) {
self.data = data
} else {
self.message = try container.decode(LoginFailure.self)
}
}
}
let success = """
{
"user_id": 41,
"token": "Token",
"token_type": "bearer",
"expires_in": 12096000
}
""".data(using: .utf8)!
let failure = """
{
"message": "this is a failure message"
}
""".data(using: .utf8)!
do {
let r = try JSONDecoder().decode(LoginRespone.self, from: failure)
print(r.message?.message)
} catch {
print(error)
}
You can use one structure to handle the response. Based on status code, you can differentaite the response.
struct LoginModel:Codable {
let message:String
let userId: Int
let token: String
let tokenType : String
let expiresIn: Int
enum CodingKeys : String,CodingKey {
case message, token
case userId = "user_id"
case tokenType = "token_type"
case expiresIn = "expires_in"
}
init(from decoder: Decoder) throws {
let value = try decoder.container(keyedBy: CodingKeys.self)
self.message = try value.decodeIfPresent(String.self, forKey: .message) ?? ""
self.userId = try value.decodeIfPresent(Int.self, forKey: .userId) ?? 0
self.token = try value.decodeIfPresent(String.self, forKey: .token) ?? ""
self.tokenType = try value.decodeIfPresent(String.self, forKey: .tokenType) ?? ""
self.expiresIn = try value.decodeIfPresent(Int.self, forKey: .expiresIn) ?? 0
}
}

Swift Decodable - How to decode nested JSON that has been base64 encoded

I am attempting to decode a JSON response from a third-party API which contains nested/child JSON that has been base64 encoded.
Contrived Example JSON
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
PS "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9" is { 'name': 'some-value' } base64 encoded.
I have some code that is able to decode this at present but unfortunately I have to reinstanciate an additional JSONDecoder() inside of the init in order to do so, and this is not cool...
Contrived Example Code
struct Attributes: Decodable {
let name: String
}
struct Model: Decodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let encodedAttributesString = try container.decode(String.self, forKey: .attributes)
guard let attributesData = Data(base64Encoded: encodedAttributesString) else {
fatalError()
}
// HERE IS WHERE I NEED HELP
self.attributes = try JSONDecoder().decode(Attributes.self, from: attributesData)
}
}
Is there anyway to achieve the decoding without instanciating the additional JSONDecoder?
PS: I have no control over the response format and it cannot be changed.
If attributes contains only one key value pair this is the simple solution.
It decodes the base64 encoded string directly as Data – this is possible with the .base64 data decoding strategy – and deserializes it with traditional JSONSerialization. The value is assigned to a member name in the Model struct.
If the base64 encoded string cannot be decoded a DecodingError will be thrown
let jsonString = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
struct Model: Decodable {
let id: Int64
let name: String
private enum CodingKeys: String, CodingKey {
case id, attributes
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributeData = try container.decode(Data.self, forKey: .attributes)
guard let attributes = try JSONSerialization.jsonObject(with: attributeData) as? [String:String],
let attributeName = attributes["name"] else { throw DecodingError.dataCorruptedError(forKey: .attributes, in: container, debugDescription: "Attributes isn't eiter a dicionary or has no key name") }
self.name = attributeName
}
}
let data = Data(jsonString.utf8)
do {
let decoder = JSONDecoder()
decoder.dataDecodingStrategy = .base64
let result = try decoder.decode(Model.self, from: data)
print(result)
} catch {
print(error)
}
I find the question interesting, so here is a possible solution which would be to give the main decoder an additional one in its userInfo:
extension CodingUserInfoKey {
static let additionalDecoder = CodingUserInfoKey(rawValue: "AdditionalDecoder")!
}
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder() //here you can put the same one, you can add different options, same ones, etc.
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
Because the main method we use from JSONDecoder() is func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable and I wanted to keep it as such, I created a protocol:
protocol BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable
}
extension JSONDecoder: BasicDecoder {}
And I made JSONDecoder respects it (and since it already does...)
Now, to play a little and check what could be done, I created a custom one, in the idea of having like you said a XML Decoder, it's basic, and it's just for the fun (ie: do no replicate this at home ^^):
struct CustomWithJSONSerialization: BasicDecoder {
func decode<T>(_ type: T.Type, from data: Data) throws -> T where T : Decodable {
guard let dict = try JSONSerialization.jsonObject(with: data) as? [String: Any] else { fatalError() }
return Attributes(name: dict["name"] as! String) as! T
}
}
So, init(from:):
guard let attributesData = Data(base64Encoded: encodedAttributesString) else { fatalError() }
guard let additionalDecoder = decoder.userInfo[.additionalDecoder] as? BasicDecoder else { fatalError() }
self.attributes = try additionalDecoder.decode(Attributes.self, from: attributesData)
Let's try it now!
var decoder = JSONDecoder()
let additionalDecoder = JSONDecoder()
decoder.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
var decoder2 = JSONDecoder()
let additionalDecoder2 = CustomWithJSONSerialization()
decoder2.userInfo = [CodingUserInfoKey.additionalDecoder: additionalDecoder]
let jsonStr = """
{
"id": 1234,
"attributes": "eyAibmFtZSI6ICJzb21lLXZhbHVlIiB9",
}
"""
let jsonData = jsonStr.data(using: .utf8)!
do {
let value = try decoder.decode(Model.self, from: jsonData)
print("1: \(value)")
let value2 = try decoder2.decode(Model.self, from: jsonData)
print("2: \(value2)")
}
catch {
print("Error: \(error)")
}
Output:
$> 1: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
$> 2: Model(id: 1234, attributes: Quick.Attributes(name: "some-value"))
After reading this interesting post, I came up with a reusable solution.
You can create a new NestedJSONDecodable protocol which gets also the JSONDecoder in it's initializer:
protocol NestedJSONDecodable: Decodable {
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws
}
Implement the decoder extraction technique (from the aforementioned post) together with a new decode(_:from:) function for decoding NestedJSONDecodable types:
protocol DecoderExtractable {
func decoder(for data: Data) throws -> Decoder
}
extension JSONDecoder: DecoderExtractable {
struct DecoderExtractor: Decodable {
let decoder: Decoder
init(from decoder: Decoder) throws {
self.decoder = decoder
}
}
func decoder(for data: Data) throws -> Decoder {
return try decode(DecoderExtractor.self, from: data).decoder
}
func decode<T: NestedJSONDecodable>(_ type: T.Type, from data: Data) throws -> T {
return try T(from: try decoder(for: data), using: self)
}
}
And change your Model struct to conform to NestedJSONDecodable protocol instead of Decodable:
struct Model: NestedJSONDecodable {
let id: Int64
let attributes: Attributes
private enum CodingKeys: String, CodingKey {
case id
case attributes
}
init(from decoder: Decoder, using nestedDecoder: JSONDecoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.id = try container.decode(Int64.self, forKey: .id)
let attributesData = try container.decode(Data.self, forKey: .attributes)
self.attributes = try nestedDecoder.decode(Attributes.self, from: attributesData)
}
}
The rest of your code will remain the same.
You could create a single decoder as a static property of Model, configure it once, and use it for all your Model decoding needs, both externally and internally.
Unsolicited thought:
Honestly, I would only recommend doing that if you're seeing a measurable loss of CPU time or crazy heap growth from the allocation of additional JSONDecoders… they're not heavyweight objects, less than 128 bytes unless there's some trickery I don't understand (which is pretty common though tbh):
let decoder = JSONDecoder()
malloc_size(Unmanaged.passRetained(decoder).toOpaque()) // 128

Decode data of Object type and generic type for base JSON response using Decodable

I have a base model -
struct BaseModel<T:Decodable>: Decodable {
let jsonData: [T]?
let status: Bool?
let message: String?
enum CodingKeys: String, CodingKey {
case jsonData = "data"
case status = "success"
case message
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
if let string = try container.decodeIfPresent(T.self, forKey: .jsonData) {
print(string)
jsonData = [string]
} else {
jsonData = nil
}
} catch DecodingError.typeMismatch {
jsonData = try container.decodeIfPresent([T].self, forKey: .jsonData)
}
status = try container.decodeIfPresent(Bool.self, forKey: .status)
message = try container.decodeIfPresent(String.self, forKey: .message)
}
}
I am getting response in two types under jsonData
Object
Array
Getting error while decoding if I receive response as Object. And if I choose let jsonData: T?, Then getting issue in decoding of Array response.
I am using this model in my Network Model. That looks like -
func performOperation<T:Decodable>(urlEndPoint: String, method: HTTPMethod, param: Parameters?, isJsonAvailable: Bool, completion: #escaping(_ response: T?, [T]?, String?, Bool?) ->Void) {
AF.request(urlEndPoint, method: method, parameters: param, headers: header).validate(statusCode: 200..<500).responseDecodable(of: BaseModel<T>.self, decoder: decoder) { (response) in
}
Json response in case of Object -
{
"success": true,
"data": {
"heading": "Same text 1",
"title": "Sample Text 2",
"content": "Sample text 3"
},
"message": "Api response received"
}
Json response in case of ArrayList -
{
"success": true,
"data": [
{
"id": 1,
"name": "Home"
},
{
"id": 2,
"name": "Profile"
}
],
"message": "Menu List"
}
You don't need a generic structure. Just create a optional property to assign your object in case there is no user array:
struct BaseModel {
let data: [User]
let post: Post?
let success: Bool
let message: String
}
struct User: Codable {
let id: Int
let name: String
}
struct Post: Codable {
let heading: String
let title: String
let content: String
}
extension BaseModel: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
data = try container.decode([User].self, forKey: .data)
post = nil
} catch DecodingError.typeMismatch {
data = []
post = try container.decode(Post.self, forKey: .data)
}
success = try container.decode(Bool.self, forKey: .success)
message = try container.decode(String.self, forKey: .message)
}
}
If there is other responses not shown in your post you can do the same approach above using a generic structure as well:
struct BaseModel<T: Codable> {
let array: [T]
let element: T?
let success: Bool
let message: String
}
extension BaseModel: Codable {
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
array = try container.decode([T].self, forKey: .array)
element = nil
} catch DecodingError.typeMismatch {
array = []
element = try container.decode(T.self, forKey: .array)
}
success = try container.decode(Bool.self, forKey: .success)
message = try container.decode(String.self, forKey: .message)
}
}

Decoding invalid URLs as nil

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

Decoding dynamic JSON structure in swift 4

I have the following issue that I'm not sure how to handle.
My JSON response can look like this:
{
"data": {
"id": 7,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NDY1MTU0NDMsImRhdGEiOiJ2bGFkVGVzdCIsImlhdCI6MTU0NjUwODI0M30.uwuPhlnchgBG4E8IvHvK4bB1Yj-TNDgmi7wUAiKmoVo"
},
"error": null
}
Or like this:
{
"data": [{
"id": 12
}, {
"id": 2
}, {
"id": 5
}, {
"id": 7
}],
"error": null
}
So in short the data can be either a single objet or an Array. What i have is this:
struct ApiData: Decodable {
var data: DataObject?
var error: String?
}
struct DataObject: Decodable {
var userId: Int?
enum CodingKeys: String, CodingKey {
case userId = "id"
}
}
This works fine for the first use case, but it will fail once data turns into
var data: [DataObject?]
How do I make that dynamic without duplicating code?
Edit: This is how i decode the object as well
func makeDataTaskWith(with urlRequest: URLRequest, completion: #escaping(_ apiData: ApiData?) -> ()) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: urlRequest) {
(data, response, error) in
guard let _ = response, let data = data else {return}
if let responseCode = response as? HTTPURLResponse {
print("Response has status code: \(responseCode.statusCode)")
}
do {
let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data)
completion(retreived)
} catch let decodeError as NSError {
print("Decoder error: \(decodeError.localizedDescription)\n")
return
}
}.resume()
}
If data can be a single object or an array write a custom initializer which decodes first an array, if a type mismatch error occurs decode a single object. data is declared as an array anyway.
As token appears only in a single object the property is declared as optional.
struct ApiData: Decodable {
let data : [DataObject]
let error : String?
private enum CodingKeys : String, CodingKey { case data, error }
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
data = try container.decode([DataObject].self, forKey: .data)
} catch DecodingError.typeMismatch {
data = [try container.decode(DataObject.self, forKey: .data)]
}
error = try container.decodeIfPresent(String.self, forKey: .error)
}
}
struct DataObject: Decodable {
let userId : Int
let token : String?
private enum CodingKeys: String, CodingKey { case userId = "id", token }
}
Edit: Your code to receive the data can be improved. You should add a better error handling to return also all possible errors:
func makeDataTaskWith(with urlRequest: URLRequest, completion: #escaping(ApiData?, Error?) -> Void) {
let config = URLSessionConfiguration.default
let session = URLSession(configuration: config)
session.dataTask(with: urlRequest) {
(data, response, error) in
if let error = error { completion(nil, error); return }
if let responseCode = response as? HTTPURLResponse {
print("Response has status code: \(responseCode.statusCode)")
}
do {
let retreived = try NetworkManager.shared.decoder.decode(ApiData.self, from: data!)
completion(retreived, nil)
} catch {
print("Decoder error: ", error)
completion(nil, error)
}
}.resume()
}
Using power of generic, it simple like below:
struct ApiData<T: Decodable>: Decodable {
var data: T?
var error: String?
}
struct DataObject: Decodable {
private var id: Int?
var userId:Int? {
return id
}
}
Use
if let obj = try? NetworkManager.shared.decoder.decode(ApiData<DataObject>.self, from: data) {
//Do somthing
} else if let array = try NetworkManager.shared.decoder.decode(ApiData<[DataObject]>.self, from: data) {
// Do somthing
}
If you have only two possible outcomes for your data, an option would be to try and parse data to one of the expected types, if that fails you know that the data is of other type and you can then handle it accordingly.
See this
You can try
struct Root: Codable {
let data: DataUnion
let error: String?
}
enum DataUnion: Codable {
case dataClass(DataClass)
case datumArray([Datum])
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
if let x = try? container.decode([Datum].self) {
self = .datumArray(x)
return
}
if let x = try? container.decode(DataClass.self) {
self = .dataClass(x)
return
}
throw DecodingError.typeMismatch(DataUnion.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for DataUnion"))
}
func encode(to encoder: Encoder) throws {
var container = encoder.singleValueContainer()
switch self {
case .dataClass(let x):
try container.encode(x)
case .datumArray(let x):
try container.encode(x)
}
}
}
struct Datum: Codable {
let id: Int
}
struct DataClass: Codable {
let id: Int
let token: String
}
let res = try? JSONDecoder().decode(Root.self, from:data)