Codable with inheritance - ios

I'm serializing and deserializing inherited classes using Codable protocol. I'm successfully able to serialize whole model into JSON but I'm having a trouble deserialize the JSON. This my data structure looks like. Some part of my application work like the below example and at this point it will be too much work for us to change the whole data structure.
class Base: Codable {
let baseValue: String
init(baseValue :String) {
self.baseValue = baseValue
}
enum SuperCodingKeys: String, CodingKey {
case baseValue
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: SuperCodingKeys.self)
try container.encode(baseValue, forKey: .baseValue)
}
}
class Child1: Base {
let child1Value: Int
init(child1Value: Int, baseValue: String) {
self.child1Value = child1Value
super.init(baseValue: baseValue)
}
private enum CodingKeys: String, CodingKey {
case child1Value
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.child1Value = try container.decode(Int.self, forKey: .child1Value)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(child1Value, forKey: .child1Value)
try super.encode(to: encoder)
}
}
class Child2: Base {
let child2Value: Int
init(child2Value: Int, baseValue: String) {
self.child2Value = child2Value
super.init(baseValue: baseValue)
}
private enum CodingKeys: String, CodingKey {
case child2Value
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.child2Value = try container.decode(Int.self, forKey: .child2Value)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(child2Value, forKey: .child2Value)
try super.encode(to: encoder)
}
}
class DataManager: Codable {
var bases: [Base] = []
init(bases: [Base]) {
self.bases = bases
}
}
This how I add value to DataManger(btw in my application this a singleton class which holds the whole data for the app. I'm trying to implement Codable protocol on this DataManager class)
let child1 = Child1(child1Value: 1, baseValue: "Child1Value_Base")
let child2 = Child2(child2Value: 2, baseValue: "Child2Value_Base")
let dataManager = DataManager(bases: [])
dataManager.bases.append(child1 as Base)
dataManager.bases.append(child2 as Base)
I'm using JSONEncoder() to encode the model into data by using this code. It's working fine up to this point.
let dataManagerData = try JSONEncoder().encode(dataManager)
print(String(data: dataManagerData, encoding: .utf8))
This what json looks like after Encoding
{
"bases":[{
"child1Value":1,
"baseValue":"Child1Value_Base"
},
{
"child2Value":2,
"baseValue":"Child2Value_Base"
}]
}
So when I try to decode this JSON by using below code I'm only able to decode it up to the Base(parent class) level, not to the child level.
let dataManager = try JSONDecoder().decode(DataManager.self, from: dataManagerData)
And this what I'm able to get out of it.
{
"bases":[{
"baseValue":"Child1Value_Base"
},
{
"baseValue":"Child2Value_Base"
}]
}
To solve this problem I tried to manually decode by using this way but JSONDecoder gives me 0 counts of Bases.
class DataManager: Codable {
var bases: [Base] = []
init(bases: [Base]) {
self.bases = bases
}
private enum CodingKeys: String, CodingKey {
case bases
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
do {
// I think decoder is trying to decode the bases object based on Child1 or Child2 class and it fail.
if let value = try? container.decode(Child1.self, forKey: .bases) {
bases.append(value as Base)
} else if let value = try? container.decode(Child2.self, forKey: .bases){
bases.append(value as Base)
}
} catch {
print(error)
}
}
}
So my questions are
how to deserialize this array of bases based on their respective child class and add them to DataManager?
Is there any way in "init(from decoder: Decoder)" method where we can get the value of key and iterate through them one by one for decoding to their respective class.

If anyone is having the same problem I found below solution.
class DataManager: Codable {
var bases: [Base] = []
init(bases: [Base]) {
self.bases = bases
}
private enum CodingKeys: String, CodingKey {
case bases
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
var list = try container.nestedUnkeyedContainer(forKey: DataManager.CodingKeys.bases)
while !list.isAtEnd {
if let child1 = try? list.decode(Child1.self) {
bases.append(child1 as Base)
} else if let child2 = try? list.decode(Child2.self) {
bases.append(child2 as Base)
}
}
}
}

Related

Asymmetric Encoding/Decoding with Codable and Firestore?

I have the following struct:
struct Recipe: Codable {
#DocumentID var id: String?
var vegetarian: Bool?
private enum CodingKeys: String, CodingKey {
case id
case vegetarian
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
vegetarian = try container.decode(Bool.self, forKey: .vegetarian)
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(id, forKey: .id)
try container.encode(vegetarian, forKey: .vegetarian)
}
}
I am trying to:
Decode only vegetarian
Encode both vegetarian and id
This is my data model:
docRef.getDocument { document, error in
if let error = error as NSError? {
self.errorMessage = "Error getting document: \(error.localizedDescription)"
}
else {
if let document = document {
do {
self.recipe = try document.data(as: Recipe.self)
let recipeFromFirestore = Recipe(
id: self.recipe!.id,
vegetarian: self.recipe!.vegetarian)
self.recipes.append(recipeFromFirestore)
}
catch {
print("Error: \(error)")
}
}
}
}
I'm getting the following error in my getDocument: Missing argument for parameter 'from' in call.
This error doesn't happen if I comment out my init(from decoder: Decoder) and func encode(to encoder: Encoder) in my struct. Should I be doing something different for this asymmetric encoding/decoding?
I don't get where you are seeing that error from - a line reference would be useful - but it's not directly related to your en/decoding. (As an aside, that method really is not a data model)
As you want to decode both properties with JSON keys that match their property names, there is no need to specify CodingKeys or write a custom decoder; you can rely on the synthesised decoder and let Codable do the work for you.
For the decoding you will need a custom solution else Decodable will decode both fields. This will require both a CodingKey enum (note the singular, i.e. the protocol, not the defaut enun name) and a custom encoder to use that.
You end up with a far simpler implementation of your struct. I've also added a simple initialiser as you lose the synthesised memberwise initialiser as soon as you define the init(from:). This was just so I could test it.
struct Recipe: Codable {
var id: String?
var vegetarian: Bool?
init(id: String, vegetarian: Bool){
self.id = id
self.vegetarian = vegetarian
}
init(from decoder: Decoder) throws {
enum DecodingKeys: CodingKey {
case vegetarian
}
let container = try decoder.container(keyedBy: DecodingKeys.self)
vegetarian = try container.decode(Bool.self, forKey: .vegetarian)
}
}
If you test this you will find that it will just decode the vegetarian property but encode both. Simple testing shows:
let recipe = Recipe(id: "1", vegetarian: true)
let data = try! JSONEncoder().encode(recipe)
print(String(data: data, encoding: .utf8)!) //{"id":"1","vegetarian":true}
let decodedRecipe = try! JSONDecoder().decode(Recipe.self, from: data)
print(decodedRecipe) //Recipe(id: nil, vegetarian: Optional(true))

How to decode json that has dynamic key values in swift?

I am having a little trouble constructing my Codable model correctly in swift. I have a json that can have dynamic id key values and one key value that I know is always the same. How do I deal with the dynamic id values? From my research, it looks like I need a custom decoder init, but I don't believe I am doing it 100% correctly. Thank you.
Example json:
{
"1f73433230": "Clark Kent",
"f1c3432fd6": "Batman",
"3d34457d69": "Wonder Woman",
"OTHER_ID": "Other"
}
Code
struct SuperHeroIds: Codable, Equatable {
let id: String
let otherId: String
enum CodingKeys: String, CodingKey {
case otherID = "OTHER_ID"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherId = try container.decode(String.self, forKey: .otherID)
id = String()
???
}
}
I would prefer
let res = try JSONDecoder().decode([String:String].self, from:data)
You can use custom CodingKey
struct AnyKey: CodingKey {
enum Errors: Error {
case invalid
}
var stringValue: String
var intValue: Int?
init?(stringValue: String) {
self.stringValue = stringValue
self.intValue = Int(stringValue)
}
init?(intValue: Int) {
self.intValue = intValue
stringValue = "\(intValue)"
}
static func key(named name: String) throws -> Self {
guard let key = Self(stringValue: name) else {
throw Errors.invalid
}
return key
}
}
and decode in init(from decoder: Decoder) throws like so:
struct SuperHeroIds: Decodable, Equatable {
let id: String
let otherId: String
let idToHeroMap: [String:String]
enum CodingKeys: String, CodingKey, CaseIterable {
case otherID = "OTHER_ID"
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
otherId = try container.decode(String.self, forKey: .otherID)
id = String()
let heroContainer = try decoder.container(keyedBy: AnyKey.self)
// all keys that not decoded with CodingKeys
let decodedKeys = CodingKeys.allCases.map({ $0.rawValue })
let filteredKeys = heroContainer.allKeys.filter({ !decodedKeys.contains($0.stringValue) })
// fill idToHeroMap
var result = [String:String](minimumCapacity: filteredKeys.count)
for key in filteredKeys {
result[key.stringValue] = try heroContainer.decode(String.self, forKey: key)
}
self.idToHeroMap = result
}
}

Is it possible to have an array of structs which conform to the same protocol also support Codable?

I have setup the following protocol, and have 2 structs which then conform to this protocol:
protocol ExampleProtocol: Decodable {
var name: String { get set }
var length: Int { get set }
}
struct ExampleModel1: ExampleProtocol {
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol {
var name: String
var length: Int
var dateString: String
}
I want to deserialise some JSON data I receive from the server, and I know it will be returning a mix of both ExampleModel1 and ExampleModel2 in an array:
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
Is there anyway to use the Codable approach and support both models easily? Or will I need to manually deserialise the data for each model?
EDIT 1:
Conforming to Decodable on the structs, still gives the same results:
struct ExampleModel1: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, otherData
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.otherData = try container.decode(Array<String>.self, forKey: .otherData)
}
var name: String
var length: Int
var otherData: Array<String>
}
struct ExampleModel2: ExampleProtocol, Decodable {
enum CodingKeys: String, CodingKey {
case name, length, dateString
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.length = try container.decode(Int.self, forKey: .length)
self.dateString = try container.decode(String.self, forKey: .dateString)
}
var name: String
var length: Int
var dateString: String
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
}
If you have a limited amount of ExampleProtocols and you need to have a different type of ExampleProtocols in the same array, then you can create a holder for ExampleProtocol and use it for decoding/encoding.
ExampleHolder could hold all possible Decodable ExampleProtocol types in one array. So decoder init don't need to have so many if-else scopes and easier to add more in the future.
Would recommend keeping ExampleHolder as a private struct. So it's not possible to access it outside of file or maybe even not outside of ExampleNetworkResponse.
enum ExampleNetworkResponseError: Error {
case unsupportedExampleModelOnDecoding
}
private struct ExampleHolder: Decodable {
let exampleModel: ExampleProtocol
private let possibleModelTypes: [ExampleProtocol.Type] = [
ExampleModel1.self,
ExampleModel2.self
]
init(from decoder: Decoder) throws {
for type in possibleModelTypes {
if let model = try? type.init(from: decoder) {
exampleModel = model
return
}
}
throw ExampleNetworkResponseError.unsupportedExampleModelOnDecoding
}
}
struct ExampleNetworkResponse: Decodable {
var someString: String
var modelArray: Array<ExampleProtocol>
enum CodingKeys: String, CodingKey {
case someString, modelArray
}
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
someString = try container.decode(String.self, forKey: .someString)
let exampleHolderArray = try container.decode([ExampleHolder].self, forKey: .modelArray)
modelArray = exampleHolderArray.map({ $0.exampleModel })
}
}
–––––––––––––––––––––––––––––––––
If in one response can have only one type of ExampleProtocol in the array then:
struct ExampleNetworkResponse2<ModelArrayElement: ExampleProtocol>: Decodable {
var someString: String
var modelArray: Array<ModelArrayElement>
}
usage:
let decoder = JSONDecoder()
let response = try decoder.decode(
ExampleNetworkResponse2<ExampleModel1>.self,
from: dataToDecode
)

How to use CodingKeys in sub-class when the super class is also Codable?

I'm working on JSON encode and decode, but some problems are very annoying and I don't know how to use CodingKeys in inherit classes.
I have two classes ResponseBean and ResponseWithObjectBean<T>.
Here is the response class definition:
public class ResponseBean: Codable
{
//This is only sample, I define `CodingKeys` because the property in json is in different name.
private enum CodingKeys: String, CodingKey
{
case intA
case intB
}
public var intA: Int32 = 0
public var intB: Int32 = 0
}
public class ResponseWithObjectBean<T: Codable> : ResponseBean
{
/*
Here I don't know how to define an enum to confirm protocl CondingKey.
I defined an enum named CodingKeys or whatever, they just don't work and
the testMessage and obj are still nil.
But if I implement the init(from decoder: Decoder) construction and manually
pass the coding keys which I defined to the decode function, all works fine.
*/
public var testMessage: String? = nil
public var obj: T? = nil
}
and I will get a user from the response:
public class User: Codable
{
private enum CodingKeys: String, CodingKey
{
case name
case age
}
public var name: String? = nil
public var age: Int32? = nil
}
Here is the test json:
var testJson = """
{
"intA": 10,
"intB": 20,
"testMessage": "This is a test json",
"obj":{
"name": "LiHong",
"age": 11
}
}
"""
The following is how I run:
do{
var responseData = testJson.data(using: .utf8)
var decoder = JSONDecoder()
var response: ResponseWithObjectBean<User> = try decoder.decode(ResponseWithObjectBean<User>.self, from: responseData)
}catch let e{
}
I don't know how to define CodingKeys in ResponseWithObjectBean class, and even I did, it dosen't work at all. But if I implement init(from decoder: Decoder) throws construction and manully pass coding keys I defined in ResponseWithObjectBean, I can get all properties.
This is pretty simple, you just have to do the coding and decoding by hand, in the child class:
public class ResponseWithObjectBean<T: Codable> : ResponseBean {
public var testMessage: String? = nil
public var obj: T? = nil
// Create another CodingKey compliant enum with another name for the new keys
private enum CustomCodingKeys: String, CodingKey {
case testMessage
case obj
}
// Override the decoder
required init(from decoder: Decoder) throws {
try super.init(from: decoder)
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
testMessage = try container.decode(String?.self, forKey: .testMessage)
obj = try container.decode(T?.self, forKey: .obj)
}
// And the coder
public override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CustomCodingKeys.self)
try container.encode(testMessage, forKey: .testMessage)
try container.encode(obj, forKey: .obj)
}
}
This way you can decode and encode the way you want:
let decoder = JSONDecoder()
let response = try decoder.decode(ResponseWithObjectBean<User>.self, from: responseData)
let data = try JSONEncoder().encode(response)
print(String(data: data, encoding: .utf8))
EDIT: In order to prevent you from writing manually all this boilerplate, you can use generation tools like Sourcery: https://github.com/krzysztofzablocki/Sourcery

Attempting to parse dynamic JSON in iOS

I have produced the following sample block of JSON. Any value that ends with a letter is dynamic.
{
"groupName": {
"groupA": {
"fields": {
"fieldA": "valueA",
"fieldB": "valueB"
},
"letters": {
"letterA: "A"
}
},
"groupB": {
"fields": {
"fieldC": "valueC",
"fieldD": "valueD"
},
"letters": {
"letterB: "B"
}
}
}
}
My goal is to use Decodable so that I may read this data into structs that I have defined.
Below is my current work contained in a playground file that I am using to try and resolve this:
import Foundation
let jsonString = "{\"groupName\":{\"groupA\":{\"fields\":{\"fieldA\":\"valueA\",\"fieldB\":\"valueB\"},\"letters\":{\"letterA:\"A\"}},\"groupB\":{\"fields\":{\"fieldC\":\"valueC\",\"fieldD\":\"valueD\"},\"letters\":{\"letterB:\"B\"}}}}"
struct CustomCodingKeys: CodingKey {
var intValue: Int?
var stringValue: String
init?(intValue: Int) { self.intValue = intValue; self.stringValue = "\(intValue)" }
init?(stringValue: String) { self.stringValue = stringValue }
static let field = CustomCodingKeys.make(key: "field")
static func make(key: String) -> CustomCodingKeys {
return CustomCodingKeys(stringValue: key)!
}
}
// Values
struct Field {
let field: String
let value: String
}
struct Letter: Decodable {
let title: String
let letter: String
}
// Value holders
struct FieldData: Decodable {
var fields: [Field]
init(from decoder: Decoder) throws {
self.fields = [Field]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
print("processing field: \(key.stringValue)")
let dynamicKey = CustomCodingKeys.make(key: key.stringValue)
let value = try container.decode(String.self, forKey: dynamicKey)
let field = Field(field: key.stringValue,
value: value)
fields.append(field)
}
}
}
struct LetterData: Decodable {
var letters: [Letter]
init(from decoder: Decoder) throws {
self.letters = [Letter]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
print("processing letter: \(key.stringValue)")
let dynamicKey = CustomCodingKeys.make(key: key.stringValue)
let value = try container.decode(String.self, forKey: dynamicKey)
let letter = Letter(title: key.stringValue,
letter: value)
letters.append(letter)
}
}
}
// Containers
struct Group: Decodable {
var name: String!
var groups: [GroupData]
init(from decoder: Decoder) throws {
self.groups = [GroupData]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
print("processing section: \(key.stringValue)")
let group = try container.decode(GroupData.self,
forKey: key)
groups.append(group)
}
}
}
struct GroupData: Decodable {
var fieldData: FieldData
var letterData: LetterData
enum CodingKeys: String, CodingKey {
case fieldData = "fields"
case letterData = "letters"
}
}
struct GroupList: Decodable {
struct GroupName: Decodable {
var name: String!
var groups: [Group]
init(from decoder: Decoder) throws {
self.groups = [Group]()
let container = try decoder.container(keyedBy: CustomCodingKeys.self)
for key in container.allKeys {
let name = key.stringValue
self.name = name
print("processing group: \(String(describing: self.name))")
var group = try container.decode(Group.self,
forKey: key)
group.name = name
groups.append(group)
}
}
}
let groupName: GroupName
}
let decoder = JSONDecoder()
if let data = jsonString.data(using: .utf8),
let groupList = try? decoder.decode(GroupList.self,
from: data) {
print("group list created")
}
In my GroupData struct, I can drop the variables and then implement init(from decoder: Decoder) throws, which when configured with the proper lookups (the FieldData & LetterData inits), can identify the correct pairs. However, it does not populate the proper value structs.
You have small mistake in decoding Group. You tend to decode all keys for group inside Group and also you pass further the to decode GroupData which itself has "fields" and "letters". Use single value container inside Group and it should be fine.
Here is how your Group should look,
struct Group: Decodable {
var name: String!
var groups: GroupData
init(from decoder: Decoder) throws {
let container = try decoder.singleValueContainer()
groups = try container.decode(GroupData.self)
}
}
Note, your json itself is incorrect, I have formatted it and it should rather be like this,
let jsonString = "{\"groupName\":{\"groupA\":{\"fields\":{\"fieldA\":\"valueA\",\"fieldB\":\"valueB\"},\"letters\":{\"letterA\":\"A\"}},\"groupB\":{\"fields\":{\"fieldC\":\"valueC\",\"fieldD\":\"valueD\"},\"letters\":{\"letterB\":\"B\"}}}}"

Resources