Combined PropertyWrapper with Codable (Swift)

Khwan Siricharoenporn
te<h @TDG

--

Hi If you want to learn new implementation solution for encode, decode json file or object. This blog will give you clear direction on how you can have a better implementation for JSON Serialize. I have collected resources and solution to help you. Then make to the new easily way for many developers that interest about exciting to code.

OK, Let’s start!!

Sections

  1. Recap about PropertyWrapper.
  2. Recap about Codable.
  3. Let’s to implement.
  4. How to use!

Section 1: Recap about PropertyWrapper

Have you ever implemented a duplicate code? Don’t worry you not alone. I found when i create the struct or class and define { get set } properties. I’ll get some duplicate code for get data from property when it has like process, especially if you need to reuse the duplicated { get set } code. You may need to create Utilize class for call in another class. Which this way you can do, But it will take your code to ugly haha. PropertyWrapper was created for fix this problem.

PropertyWrapper is a feature of Swift language version 5. This allows us to define new custom type. A process of PropertyWrapper contain some logics or methods that each property can use and reuse in everywhere without duplicate code. Just implement PropertyWrapper type at front or above of property.

— PropertyWrapper in Swift —

  • @dynamic
  • @objc

You can read more about PropertyWrapper by follow this clicking.

Just section 1. it will make explosion in your head.

Section 2: Recap about Codable

For this section. A newbie iOS developer should to learn about Codable. I have attached recap below.

Codable in Swift to encode and decode custom data formats, such as JSON, to native Swift objects. It’s incredibly easy to map Swift objects to JSON data, and vice versa, simply by adopting the Codable protocol. (Thanks for recap from this link)

Section 3: Let’s to implement

I will explain about information below of code.

Step 1.

Create a new project.

Step 2.

Create a new file and define class name is DynamicCodingKeys. Copy & Paste my code into this file.

struct DynamicCodingKeys: CodingKey {
var stringValue: String
var intValue: Int?
init(key: String) {
stringValue = key
}
init?(stringValue: String) {
self.stringValue = stringValue
}
init?(intValue: Int) {
self.intValue = intValue
self.stringValue = String(intValue)
}
}

DynamicCodingKeys class is a JSON key transformer. It will trans each key for Encoder container, Decoder container. This class has responsible for receive key from somewhere that use PropertyWrapper Serialize.

Step 3.

Create the protocols about Encodable. (JSON → Object)

protocol EncodableKey {
typealias EncodeContainer = KeyedEncodingContainer<DynamicCodingKeys>
func encodeValue(from container: inout EncodeContainer) throws
}

KeyedEncodingContainer is a generic class that specify type of CodingKey. You must use DynamicCodingKeys for specify generic type of KeyedEncodingContainer, because KeyedEncodingContainer will bring DynamicCodingKeys for find key of each json element.

protocol SuperEncodable: Encodable {}extension SuperEncodable { 
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: DynamicCodingKeys.self)
for child in Mirror(reflecting: self).children {
guard let encodableKey = child.value as? EncodableKey else { continue }
try encodableKey.encodeValue(from: &container)
}
}
}

SuperEncodable is a child of Encodable. This protocol inherit all abilities that Encodable can do. But i did re-implement `encode(to encoder:) throws` method. I use this method to increase ability about encode without CodingKeys implementation. I will just use Mirror to find properties name in some class that use PropertyWrapper Serialize. This method will map or match a name from container. Then transform from JSON element to property according found key.

Step 4.

Create the protocols about Decodable. (Object → JSON)

protocol DecodableKey { 
typealias DecodeContainer = KeyedDecodingContainer<DynamicCodingKeys>
func decodeValue(from container: DecodeContainer) throws
}

KeyedDecodingContainer is a generic class that specify type of CodingKey. You must use DynamicCodingKeys for specify generic type of KeyedDecodingContainer. Because KeyedDecodingContainer will bring DynamicCodingKeys for find key of each property.

protocol SuperDecodable: Decodable {
init()
}
extension SuperDecodable {
init(from decoder: Decoder) throws {
self.init()
let container = try decoder.container(keyedBy: DynamicCodingKeys.self) for child in Mirror(reflecting: self).children {
guard let decodableKey = child.value as? DecodableKey else { continue }
try decodableKey.decodeValue(from: container)
}
}
}

SuperDecodable is a child of Decodable. This protocol inherit all abilities that Decodable can do. But i did re-implement `init(from decoder: ) throws` method. I use this method to increase ability about decode without CodingKeys implementation. I will just use Mirror to find properties name in some class that use PropertyWrapper Serialize. This method will map or match a name from container. Then transform from property to JSON element according found key.

Step 5.

Wrap both protocols together by use TypeAlias.

typealias SuperCodable = SuperEncodable & SuperDecodable

Step 6.

Create PropertyWrapper Serialize class.

@propertyWrapper
final class Serialize<Value> {
let key: String
var wrappedValue: Value?
init(_ key: String) {
self.key = key
self.wrappedValue = nil
}
}

This is a Serialize generic class and it is a new customised PropertyWrapper type.

Property responsibility

Key: Is a name in json element or property

  • JSON Case: “id”: “123AA” → id is key
  • Property Case: var id: String? → id is key

WrappedValue: Is a value in json element or property

  • JSON Case: “id”: “123AA” → 123AA is wrappedValue
  • Property Case: var id: String? → In this case wrappedValue is 123AA.

Step 7.

Write an extension into Serialize<Value> class.

extension Serialize: EncodableKey where Value: Encodable {
func encodeValue(from container: inout EncodeContainer) throws {
let codingKey = DynamicCodingKeys(key: key)
try container.encodeIfPresent(wrappedValue, forKey: codingKey)
}
}

This extension has conformed EncodableKey and it will allow to use when Value is only Encodable.

`encodeValue(from container: ) throws` will be called automatically when some property encoding. By Serialize will bring a key property pass into DynamicCodingKeys for generate json key before pass to container. And bring wrappedValue pass to container and trans to json value for each property of some class.

extension Serialize: DecodableKey where Value: Decodable {
func decodeValue(from container: DecodeContainer) throws {
let codingKey = DynamicCodingKeys(key: key)

if let value = try container.decodeIfPresent(Value.self, forKey: codingKey) {
wrappedValue = value
}
}
}

This extension has conformed DecodableKey and it will allow to use when Value is only Decodable.

`decodeValue(from container: ) throws` will be called automatically when some property decoding. By Serialize will bring a key property pass into DynamicCodingKeys for generate json key before pass to container. And it will decode value according Value type by use key to specify json element. And if value not nil. Value will be assign to wrappedValue for each property of some class.

Symptom after read ‘Let’s to implement’.

Section 4: How to use!

Step 1.

I have created dummy json file as following.

/* You must create 'students.json' and pasted into project*/
[
{
"id": "1",
"name": "Josh",
"grade": 3.18
},
{
"id": "2",
"name": "Marc",
"grade": 2.25
},
{
"id": "3",
"name": "Judy",
"grade": 4.00
}
]

Remark: You must create ‘students.json’ and pasted into project

Step 2.

Create JSONUtils class for read json file from project bundle at Runtime process.

class JSONUtils {
static func jsonFromBundle<T: Decodable>(_ bundle: Bundle, jsonNamed named: String, responseType: T.Type) -> T? {
guard let url = bundle.url(forResource: named, withExtension: "json") else { return nil }
guard let data = try? Data(contentsOf: url) else { return nil } let decoder = JSONDecoder()
var decoded: T?
do {
decoded = try decoder.decode(T.self, from: data)
}
catch {
print("JSONUtils jsonFromBundle<T> error: ", error.localizedDescription)
}
return decoded
}
}

Step 3.

Create struct or class and conform SuperCodable protocol. Write according this pattern.

struct Student: SuperCodable {
@Serialize("id") //--> key
var id: String?
@Serialize("name") //--> key
var name: String?
@Serialize("grade") //--> key
var grade: Double?
}

Remark: Write `@Serialize(_ key: )` at front or above property.

Step 4.

Test on ViewController.

class ViewController: UIViewController {
var students: [Student]?
override func viewDidLoad() {
super.viewDidLoad()
decode()
encode()
}
}
//MARK: SuperCodable
extension ViewController {
func decode() {
students = JSONUtils.jsonFromBundle(.main, jsonNamed: "students", responseType: [Student].self)
print("============================")
print("Students json after decode:\n")
students?.forEach {
print("id: \($0.id ?? ""),\nname: \($0.name ?? ""),\ngrade: \($0.grade ?? 0)\n")
}

print("============================")
}
func encode() {
guard let students = students else { return }
let encode = JSONEncoder() guard let data = try? encode.encode(students) else { return } print("\n\n")
print("============================")
print("Students json after encode: \(data)")
print("============================")
}
}

You will get the result like the image below.

Hopefully can apply this blog and tutorial according by your work, Please share your idea or the comment below.

Please clap and share.

Thank you~~

--

--

Khwan Siricharoenporn
te<h @TDG

iOS Developer @Central Group. Interested about core code, low-level, design pattern and architecture.