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))
Related
Say we've got a cursor based paginated API where multiple endpoints can be paginated. The response of such an endpoint is always as follows:
{
"nextCursor": "someString",
"PAYLOAD_KEY": <generic response>
}
So the payload always returns a cursor and the payload key depends on the actual endpoint we use. For example if we have GET /users it might be users and the value of the key be an array of objects or we could cal a GET /some-large-object and the key being item and the payload be an object.
Bottom line the response is always an object with a cursor and one other key and it's associated value.
Trying to make this generic in Swift I was thinking of this:
public struct Paginable<Body>: Codable where Body: Codable {
public let body: Body
public let cursor: String?
private enum CodingKeys: String, CodingKey {
case body, cursor
}
}
Now the only issue with this code is that it expects the Body to be accessible under the "body" key which isn't the case.
We could have a struct User: Codable and the paginable specialized as Paginable<[Users]> where the API response object would have the key users for the array.
My question is how can I make this generic Paginable struct work so that I can specify the JSON payload key from the Body type?
The simplest solution I can think of is to let the decoded Body to give you the decoding key:
protocol PaginableBody: Codable {
static var decodingKey: String { get }
}
struct RawCodingKey: CodingKey, Equatable {
let stringValue: String
let intValue: Int?
init(stringValue: String) {
self.stringValue = stringValue
intValue = nil
}
init(intValue: Int) {
stringValue = "\(intValue)"
self.intValue = intValue
}
}
struct Paginable<Body: PaginableBody>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
body = try container.decode(Body.self, forKey: RawCodingKey(stringValue: Body.decodingKey))
cursor = try container.decodeIfPresent(String.self, forKey: RawCodingKey(stringValue: "nextCursor"))
}
}
For example:
let jsonString = """
{
"nextCursor": "someString",
"PAYLOAD_KEY": {}
}
"""
let jsonData = Data(jsonString.utf8)
struct SomeBody: PaginableBody {
static let decodingKey = "PAYLOAD_KEY"
}
let decoder = JSONDecoder()
let decoded = try? decoder.decode(Paginable<SomeBody>.self, from: jsonData)
print(decoded)
Another option is to always take the "other" non-cursor key as the body:
struct Paginable<Body: Codable>: Codable {
public let body: Body
public let cursor: String?
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: RawCodingKey.self)
let cursorKey = RawCodingKey(stringValue: "nextCursor")
cursor = try container.decodeIfPresent(String.self, forKey: cursorKey)
// ! should be replaced with proper decoding error thrown
let bodyKey = container.allKeys.first { $0 != cursorKey }!
body = try container.decode(Body.self, forKey: bodyKey)
}
}
Another possible option is to pass the decoding key directly to JSONDecoder inside userInfo and then access it inside init(from:). That would give you the biggest flexibility but you would have to specify it always during decoding.
You can use generic model with type erasing, for example
struct GenericInfo: Encodable {
init<T: Encodable>(name: String, params: T) {
valueEncoder = {
var container = $0.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: . name)
try container.encode(params, forKey: .params)
}
}
// MARK: Public
func encode(to encoder: Encoder) throws {
try valueEncoder(encoder)
}
// MARK: Internal
enum CodingKeys: String, CodingKey {
case name
case params
}
let valueEncoder: (Encoder) throws -> Void
}
Below is my model struct
struct MovieResponse: Codable {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
extension MovieResponse {
struct Movie: Codable, Identifiable {
var id = UUID()
var title: String
var year: Int8
var imdbID: String
var type: String
var poster: URL
enum EncodingKeys: String, CodingKey {
case title = "Title"
case year = "Year"
case imdmID
case type = "Type"
case poster = "Poster"
}
}
}
Now in a ViewModel, I am creating an instance of this model using the below code
#Published var movieObj = MovieResponse()
But there is a compile error saying, call init(from decoder) method. What is the proper way to create a model instance in this case?
As the Swift Language Guide reads:
Swift provides a default initializer for any structure or class that provides default values for all of its properties and doesn’t provide at least one initializer itself.
The "and doesn’t provide at least one initializer itself" part is crucial here. Since you are declaring an additional initializer you should either declare your own initialized like so:
init(
totalResults: Int,
response: String,
error: String,
movies: [Movie]
) {
self.totalResults = totalResults
self.response = response
self.error = error
self.movies = movies
}
or move Codable conformance to an extension so Swift can provide you with a default initialiser. This would be a preferred way to do it (my personal opinion, I like to move additional protocol conformances to extensions).
struct MovieResponse {
var totalResults: Int
var response: String
var error: String
var movies: [Movie]
}
extension MovieResponse: Codable {
enum ConfigKeys: String, CodingKey {
case totalResults
case response = "Response"
case error = "Error"
case movies
}
init(from decoder: Decoder) throws {
let values = try decoder.container(keyedBy: CodingKeys.self)
self.totalResults = try values.decodeIfPresent(Int.self, forKey: .totalResults)!
self.response = try values.decodeIfPresent(String.self, forKey: .response)!
self.error = try values.decodeIfPresent(String.self, forKey: .error) ?? ""
self.movies = try values.decodeIfPresent([Movie].self, forKey: .movies)!
}
}
You need to add another initializer if you do not want to use a decoder. Swift give you one for free if and only if you do not write your own initializer. Now that you have one you loose the free one.
add another:
init() {
//Your initializer code here
}
If you are trying to use the decoder init you need to use a decoder to invoke it. For instance if it's Json
#Published var movieObj = try? JSONDecoder().decode(MovieResponse.self, from: <#T##Data#>)
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
}
}
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)
}
}
}
}
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