DynamicMapper
DynamicMapper copied to clipboard
Dynamic decoding and encoding using native Codable protocol
DynamicMapper
DynamicMapper is a framework written in Swift for dynamically decoding and encoding models (reference and value types) using Swift native Decodable and Encodable protocols.
- Installation
- Features
- The Basics
- Easy Transformation from
Codable - Mapping Nested Objects
- Easy Json Insersion
- DynamicMapper + Realm
- Contributing
- Author
- License
Example
To run the example project, clone the repo, and run pod install from the Example directory first.
Installation
Cocoapods
DynamicMapper is available through CocoaPods. To install it, simply add the following line to your Podfile:
pod 'DynamicMapper', '~> 2.0.1' (check releases to make sure this is the latest version)
Swift Package Manager
To add DynamicMapper to a Swift Package Manager based project, add:
.package(url: "https://github.com/AbdulrahmanQasem95/DynamicMapper.git", .upToNextMajor(from: "2.0.1")),
to your Package.swift files dependencies array.
Features:
- Decoding and encoding using native
JSONDecoderandJSONEncoder - Has the full functionality of
Codable = Decodable & Encodableprotocols - Nested Objects encoding and decoding (Dynamic Mapping)
- Rreference & Value types support
- Dynamic object insertion and creation
- Safely nested object fetching
- Support subclassing
- Smooth transformation since it works directly with your
Codablemodels without any changes - Compatible with Realm
- Native replacment of ObjectMapper
- Support Back to iOS 11, macOS 10.13, tvOS 11, and watchOS 4.
The Basics
Like native JSONDecoder and JSONEncoder To support dynamic mapping, a class or struct just needs to implement the DynamicDecodable protocol for decoding, DynamicEncodable protocol for encoding or DynamicCodable protocol for both decoding and encoding togoether
protocol DynamicCodable:DynamicDecodable,DynamicEncodable
var dynamicSelf: DynamicClass?
func dynamicMapping(mappingType: DynamicMappingType) {}
DynamicMapper uses the <-- operator and ds to set nested member variable.
ds is a safe non optional alias of dynamicSelf
class User: DynamicDecodable {
var dynamicSelf: DynamicClass?
var username: String?
var lastname: String?
var age: Int?
var weight: Double!
var birthday: Date?
var bestFamilyPhoto:URL? // Nested URL
var numberOfChildren:Int? // Nested Int
var bestFriend: User? // Nested User object
var friends: [User]? // Array of Users
func dynamicMapping(mappingType: DynamicMappingType) {
bestFamilyPhoto <-- ds.alboms.familyAlbom.bestPhoto
numberOfChildren <-- ds.familyInfo.childrenCount
bestFriend?.dynamicMapping(mappingType: mappingType)
friends?.dynamicMapping(mappingType: mappingType)
}
}
struct Temperature: DynamicDecodable {
var dynamicSelf: DynamicClass?
var celsius: Double?
var fahrenheit: Double?
mutating func dynamicMapping(mappingType: DynamicMapper.DynamicMappingType) {
// nothing to do with model's level parameters, Codable will take care of them
// unless you want to use custom name
}
}
// Custom names
struct Temperature: DynamicDecodable {
var dynamicSelf: DynamicClass?
var celsiusTemperature: Double?
var fahrenheitTemperature: Double?
mutating func dynamicMapping(mappingType: DynamicMappingType) {
celsiusTemperature <-- ds.celsius
fahrenheitTemperature <-- ds.fahrenheit
}
}
Once your class implements DynamicDecodable it can be easily decoded using DynamicJSONDecoder class.
Decoding:
do {
let userModel = try DynamicJSONDecoder().decode(User.self, from: userDataFromServer)
} catch {
print(error.localizedDescription)
}
Encoding:
do {
let userData = try DynamicJSONEncoder().encode(userModel)
} catch {
print(error.localizedDescription)
}
DynamicMapper can decode and encode all types supported by JSONDecoder and JSONEncoder:
StringIntFloatDoubleBoolArray(as long as the elements are alsoCodable)Dictionary(as long as the keys and values areCodable)- Custom structs and classes that conform to
Codable - Optional types that conform to
Codable - Enumerations with associated values (as long as the associated values are
Codable) DateURLData
Easy Transformation from Codable
Since DynamicJSONDecoder inherit JSONDecoder and DynamicJSONEncoder inherit JSONEncoder, all your Codable classes will work same like before with the new dynamic decoder and encoder without any changes.
This will allow you to move smoothly and easily from ordinary Codable to DynamicCodable and you can even mix them togethor.
You can keep the ordinary Codable and use DynamicCodable only for models where you need to access nested object without defining the implecit objects or where you need to use custom names ...
{
"title": "Swift",
"category": "Programming Languages",
"teacher": {
"firstName": "Abdulrahman",
"lastName": "Qasem"
}
}
class Subject:Codable {
var title:String
var category:String
var teacher:User?
}
class User:Codable {
var firstName:String
var lastName:String
}
do {
let subjectModel = try DynamicJSONDecoder().decode(Subject.self, from: subjectData)
print(subjectModel.teacher.firstName) //Abdulrahman
} catch {
print(error.localizedDescription)
}
or using DynamicMapper
class Subject:DynamicCodable {
var dynamicSelf: DynamicClass?
var title:String
var category:String
var teacherName:String? //Abdulrahman
func dynamicMapping(mappingType: DynamicMappingType) {
teacherName <-- ds.teacher.firstName
}
}
do {
let subjectModel = try DynamicJSONDecoder().decode(Subject.self, from: subjectData)
print(teacherName)
} catch {
print(error.localizedDescription)
}
DynamicCodable Protocol
mutating func dynamicMapping(mappingType:DynamicMappingType)
This function is where all nested items and models definitions should go. this function is executed during encoding and decoding proccess. It is the only function that is called on the object.
var dynamicSelf:DynamicClass?
Dynamic copy of the object that we will use to dynamically access the nested properties or models, we can use either dynamicSelf or its safe non optional alias ds to access the dynamic model when dynamicMapping(mappingType:DynamicMappingType) function get called
| DynamicCodable | |
|---|---|
| Properties | |
var bestFamilyPhoto: URL? var numberOfChildren: Int? |
|
| Data -> Model (Decoding) | |
func dynamicMapping(mappingType: DynamicMappingType) {
bestFamilyPhoto
|
|
| Model -> Data (Encoding) | |
func dynamicMapping(mappingType: DynamicMappingType) {
bestFamilyPhoto --> {ds.alboms.familyAlbom.bestPhoto.set($0)}
numberOfChildren --> {ds.familyInfo.childrenCount.set($0)}
}
|
|
| Decoding & Encoding | |
func dynamicMapping(mappingType: DynamicMappingType) {
switch mappingType {
case .decoding:
bestFamilyPhoto {ds.alboms.familyAlbom.bestPhoto.set($0)}
numberOfChildren --> {ds.familyInfo.childrenCount.set($0)}
}
}
|
|
Easy Mapping of Nested Objects
DynamicMapper supports dot notation within keys for easy mapping of nested objects. Given the following JSON String:
{
"property0": "Value 0",
"level1": {
"property1": "Value 1",
"level2": {
"property2": 10,
"level3": {
"property3": "Value 3",
"level4": {
"property4": "Value 4",
"level5": {
"property5": "Value 5",
"level6Array": [
{
"item1": "Item A",
"item2": 100
},
{
"item1": "Item C",
"item2": 200
},
{
"item1": "Item E",
"item2": 300
}
]
}
}
}
}
}
}
You can access the nested objects as follows:
var property2:Int?
func dynamicMapping(mappingType: DynamicMappingType) {
property2 <-- ds.level1.level2.property2
}
Nested keys also support accessing values from an array
var secondArrayItem_1_OfLevel_6:String?
func dynamicMapping(mappingType: DynamicMappingType) {
secondArrayItem_1_OfLevel_6 <-- ds.level1.level2.level3.level4.level5.level6Array[1].item1
}
fetching array item by index is safe
// this will do nothing
var secondArrayItem_1_OfLevel_6:String?
func dynamicMapping(mappingType: DynamicMappingType) {
secondArrayItem_1_OfLevel_6 <-- ds.level1.level2.level3.level4.level5.level6Array[1200].item1
}
You can access nested custom model as follows:
var level_6_Array:[ArrayItemModel]?
func dynamicMapping(mappingType: DynamicMappingType) {
level_6_Array <-- ds.level1.level2.level3.level4.level5.level6Array
}
struct ArrayItemModel:DynamicCodable{
var dynamicSelf:DynamicClass?
var item1:String
var item2:Int
func dynamicMapping(mappingType: DynamicMappingType) {}
}
Easy Json Insersion
Using the same Json above, You can insert any object or array same like when you access it
ds.level1.level2.level3.insertedProperty.set("nested item")
ds.level1.level2.insertedArray.set(["inserted item 1","inserted item 2"])
ds.level1.level2.level3.level4.level5.level6Array[4].set(ArrayItemModel(item1: "inserted item 1", item2: 5))
json will be
{
"property0": "Value 0",
"level1": {
"property1": "Value 1",
"level2": {
"property2": 10,
"insertedArray": [
"inserted item 1",
"inserted item 2"
],
"level3": {
"property3": "Value 3",
"insertedProperty": "nested item",
"level4": {
"property4": "Value 4",
"level5": {
"property5": "Value 5",
"level6Array": [
{
"item1": "Item A",
"item2": 100
},
{
"item1": "Item C",
"item2": 200
},
{
"item1": "Item E",
"item2": 300
},
{
"item1": "inserted item 1",
"item2": 5
}
]
}
}
}
}
}
}
Extend JSONDecoder & JSONEncoder
DynamicJSONDecoder and DynamicJSONEncoder inherit JSONDecoder and JSONEncoder respectively, so they have the full power of native Codable
,for example date decoding
{
"user":{
"personalInfo":{
"birthDate": "2023-10-30T19:00:00Z"
.
.
.
}
.
.
.
}
.
.
.
}
let decoder = DynamicJSONDecoder()
decoder.dateDecodingStrategy = .iso8601
var birthDate:Date?
func dynamicMapping(mappingType: DynamicMappingType) {
birthDate <-- ds.user.personalInfo.birthDate
}
DynamicMapper + Realm
DynamicMapper and Realm can be used together
import RealmSwift
class Model: Object, DynamicCodable {
var dynamicSelf:DynamicClass?
@objc dynamic var name:String? = ""
func dynamicMapping(mappingType: DynamicMappingType) {
switch mappingType {
case .decoding:
name <-- ds.user.personalInfo.name
case .encoding:
name --> {ds.user.personalInfo.name.set($0)}
}
}
}
Contributing
Contributions are very welcome 👍😃.
Before submitting any pull request, If you are including new functionality, please write test cases for it.
Author
Abdulrahman Qasem, [email protected]
License
DynamicMapper is available under the MIT license. See the LICENSE file for more info.