How to Modify Codable Class Properties

Updating a saved Codable struct to add a new property

As mentioned in the comments you can make the new property optional and then decoding will work for old data.

var newProperty: Int?

Another option is to apply a default value during the decoding if the property is missing.

This can be done by implementing init(from:) in your struct

public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
name = try container.decode(String.self, forKey: .name)
completedGame = try container.decode(Bool.self, forKey: .completedGame)
do {
newProperty = try container.decode(Int.self, forKey: .newProperty)
} catch {
newProperty = -1
}
levelScores = try container.decode([LevelScore].self, forKey: .levelScores)
}

This requires that you define a CodingKey enum

enum CodingKeys: String, CodingKey {
case name, completedGame, newProperty, levelScores
}

A third option if you don't want it to be optional when used in the code is to wrap it in a computed non-optional property, again a default value is used. Here _newProperty will be used in the stored json but newProperty is used in the code

private var _newProperty: Int?
var newProperty: Int {
get {
_newProperty ?? -1
}
set {
_newProperty = newValue
}
}

Using Codable to set property values doesn't work through inheritance

The magic of Codable relies on simplicity (using structs which don't support inheritance).

The more custom, the more code

You have to write a custom initializer in the subclass to consider inheritance (thanks to Hamish for the note that CodingKeys and initializer are synthesized in the base class), I omitted the Encodable part intentionally

class Primary : Decodable {
var a: Int

/*
private enum CodingKeys: String, CodingKey { case a }

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
a = try container.decode(Int.self, forKey: .a)
}
*/
}

class Secondary : Primary {
var b: Int

private enum CodingKeys: String, CodingKey { case b }

required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
b = try container.decode(Int.self, forKey: .b)
try super.init(from: decoder)
}
}

func testRef<T: Decodable>() throws -> T {
let json = "{\"a\":5, \"b\" : 10}".data(using: .utf8)!
return try JSONDecoder().decode(T.self, from: json)
}

do {
let secondary : Secondary = try testRef()
print(secondary.a, secondary.b) // 5 10
} catch { print(error) }

Codable custom class type property can not initialize its own properties from JSON

In Article.swift try change the following lines

category = try Category(from: decoder)
main_image_urls = try Main_image_urls(from: decoder)

to

category = values.decodeIfPresent(Category.self, forKey: .category)
main_image_urls = values.decodeIfPresent(Main_image_urls.self, forKey: .main_image_urls)

Your class Main_image_urls should also conform to codable.

You could also try to omit the init(from decoder: Decoder) methods and let the compiler synthesize it. This also works when having a custom CodingKeys enum.

Update:

Also change the following line in Category.swift

parent_category = try Parent_category(from: decoder)

to

parent_category = values.decodeIfPresent(Parent_category.self, forKey: .parentCategory)

Setting or overwriting properties of generic codable objects

Remember, the generated Something.encode(to:) method will call encoder.container(keyedBy: CodingKeys.self) to get a container, and it will encode its properties into that container. So you need to make both object and update use the same container to encode themselves. Then maybe update's properties will overwrite object's properties. On the other hand, maybe the encoder will throw an error. Let's try it and see.

To make them use the same container, we'll create a wrapper type that holds the object and the update.

import Foundation

public struct Something : Codable
{
public var property1:String?
public var property2:String?
}

struct UpdateWrapper {
var something: Something
var update: Something
}

Now we will make UpdateWrapper conform to Encodable. If we let the compiler generate the conformance, it will look like this:

// What the compiler would generate, but NOT what we want.

extension UpdateWrapper: Encodable {

enum CodingKeys: String, CodingKey {
case something
case update
}

func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(something, forKey: .something)
try container.encode(update, forKey: .update)
}

}

The reason we don't want this is because this will result in something and update getting separate containers from the encoder, and we want them to go into the same container. So instead we'll conform to Encodable like this:

extension UpdateWrapper: Encodable {
func encode(to encoder: Encoder) throws {
try something.encode(to: encoder)
try update.encode(to: encoder)
}
}

In this way, something and encoder each receive the same Encoder, with the same codingPath, so they get the same container from the encoder. We can test it like this:

let original = Something(property1: "A", property2: "B")
let update = Something(property1: "C", property2: nil)

let encoder = JSONEncoder()
let jsonData = try! encoder.encode(UpdateWrapper(something: original, update: update))
let jsonString = String(data: jsonData, encoding: .utf8)!
print("jsonString=\(jsonString)")

let decoder = JSONDecoder()
let reconstructed = try! decoder.decode(Something.self, from: jsonData)
print("reconstructed=\(reconstructed)")

Here's the output:

jsonString={"property2":"B","property1":"C"}
reconstructed=Something(property1: Optional("C"), property2: Optional("B"))

So it does work. Is it a good idea? I don't know. I'm not sure it's specified anywhere that you're actually allowed to encode with the same key into the same container twice.

How to add custom transient property in Codable struct

If you don't want to decode those 4 properties, just don't include them in the CodingKeys (and don't forget to explicitly provide default values for them, so that the decoder can initialize the object properly):

struct VideoAlbum: Codable {

let id, image: String?
let video, mediaType: JSONNull?
let type, deleted, createdOn: String?
let modifiedOn: JSONNull?

var isPlaying: Bool? = nil
var currentTime: CMTime? = nil
var timeObserver: Any? = nil
var pausedByUser: Bool? = nil

enum CodingKeys: String, CodingKey {
// include only those that you want to decode/encode
case id, image, video
case mediaType = "media_type"
case type, deleted
case createdOn = "created_on"
case modifiedOn = "modified_on"
}
}

Decoding manually a non Codable property of a Codable class

Yes you can.

The error is that you're missing func encode(to encoder: Encoder) throws in ClassA. Codable = Encodable & Decodable, so it's trying to find a way to encode ClassA as well. ClassB isn't encodable so it can't do it automatically, and you haven't told it how to do it manually either.

If you don't need to encode ClassA instances, just make it Decodable. Otherwise implement the missing encode func.

Or just put in the work and make ClassB codable as well. You can use an extension to add it after the fact. If you don't want to do that either, a workaround I've used is to declare a small private codable struct inside ClassA like struct ClassBInfo: Codable. Use that to get the info you need, then read its properties to init ClassB.



Related Topics



Leave a reply



Submit