glideapps / quicktype

Generate types and converters from JSON, Schema, and GraphQL
https://app.quicktype.io
Apache License 2.0
12.43k stars 1.08k forks source link

Adding support for Encode and Decode Manually in Swift #920

Closed lohenyumnam closed 6 years ago

lohenyumnam commented 6 years ago

Hello Guys. Will guys please add support for Manual Encode and Decode in swift

Example code:

import Foundation
struct Welcome: Codable {
    let greeting : String?
    let instructions : [String]?

    enum CodingKeys: String, CodingKey {

        case greeting = "greeting"
        case instructions = "instructions"
    }

    // Decode Manually

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)
        greeting = try values.decodeIfPresent(String.self, forKey: .greeting)
        instructions = try values.decodeIfPresent([String].self, forKey: .instructions)
    }

    //Encode Manually
    func encode(to encoder: Encoder) throws {
        var container = encoder.container(keyedBy: CodingKeys.self)
        try container.encode(greeting, forKey: .greeting)
        try container.encode(instructions, forKey: .instructions)
    }

}
schani commented 6 years ago

@lohenyumnam Why do you need this feature?

lohenyumnam commented 6 years ago

If the structure of our Swift type differs from the structure of its encoded form, we can provide a custom implementation of Encodable and Decodable to define our own encoding and decoding logic.

for example

the server sends JSON data as below

{
"Id": "103",
"Name": "Power Boy",
"designation": "Developer",
"photo_url": "images/noimage.jpg",
"TimeIn": "09:42 am",
"TimeOut": "05:35 pm"
}

The id is a string but I want it as Int in my swift type so I have to Manually Implement the code as bellow

struct People: Codable {
    var timeOut: String
    var timeIn: String
    var photo_url: String
    var designation: String
    var name: String
    var id: Int

    enum CodingKeys: String, CodingKey {
        case timeOut = "TimeOut"
        case timeIn = "TimeIn"
        case photo_url = "photo_url"
        case designation = "designation"
        case name = "Name"
        case id = "Id"
    }

    init(from decoder: Decoder) throws {
        let value = try decoder.container(keyedBy: CodingKeys.self)

        timeOut = try value.decode(String.self, forKey: CodingKeys.timeOut)
        timeIn = try value.decode(String.self, forKey: CodingKeys.timeIn)
        photo_url = try value.decode(String.self, forKey: CodingKeys.photo_url)
        designation = try value.decode(String.self, forKey: CodingKeys.designation)
        name = try value.decode(String.self, forKey: CodingKeys.name)

        // This will be auto-generated
        //id = try value.decode(String.self, forKey: CodingKeys.id) 
       // Now all I have to do is add Int
        id = Int(try value.decode(String.self, forKey: CodingKeys.id))!
    }
}
schani commented 6 years ago

quicktype already supports decoding stringified integers, but only in C# yet. It's built on data transformers, #466.

Would you be interested in contributing?

lohenyumnam commented 6 years ago

@schani Thanks for the reply, but I am not familiar with TypeScript, sorry

schani commented 6 years ago

TypeScript is pretty easy to pick up, particular if you already know Swift.

dvdsgl commented 6 years ago

I need some more examples of why this is desirable. I understand the use case cited, but this is not a good solution for that issue (we have stringified integer influence).

I'm happy to re-open the issue if more examples of why this feature is desirable are given.

lohenyumnam commented 6 years ago

The following example is the extreme case, and will always need a little or more modification from the generated code. I know that this method is all about taking control of how we want to decode the JSON data, but by adding the option to add Method init(from decoder: Decoder) while generating makes life more simpler

Example 1

Let take a simple example of the below JSON data,

[
  {
    "name": "Russ Abbot",
    "sex": "Male",
    "wife": "Selena Gomez"
  },
  {
    "name": "Demi Lovato",
    "sex": "Male",
    "husband": "Zak Abel"
  },
  {
    "name": "Toss bot",
    "sex": "Male",
    "wife": "Lovato Gomez"
  },
  {
    "name": "Demi Lovato",
    "sex": "Male",
    "husband": "Sak Tom"
  }
]

As we can see this JSON contain two different data types, one for male and one for female, where female profile contain Name of Husband and for the male name of the wife

If Quicktype Generate this it will look like this.

typealias Profile = [ProfileElement]

struct ProfileElement: Codable {
    let name: String
    let sex: String
    let wife: String?
    let husband: String?

    enum CodingKeys: String, CodingKey {
        case name = "name"
        case sex = "sex"
        case wife = "wife"
        case husband = "husband"
    }
}

OutPut:

ProfileElement(name: "Russ Abbot", sex: "Male", wife: Optional("Selena Gomez"), husband: nil)
ProfileElement(name: "Demi Lovato", sex: "Male", wife: nil, husband: Optional("Zak Abel"))
ProfileElement(name: "Toss bot", sex: "Male", wife: Optional("Lovato Gomez"), husband: nil)
ProfileElement(name: "Demi Lovato", sex: "Male", wife: nil, husband: Optional("Sak Tom"))

when decoding the jsonData profile for both Male and female will contain Husband and Wife, with one of them being nil, which doesn't make sense to me.

So, With the power of init(from decoder: Decoder) Method what I can do is I will reduce the name of wife and Husband to just one value as a "spouse".

typealias Profile = [ProfileElement]

struct ProfileElement: Codable {
    let name: String
    let sex: String
    let spouse: String

    enum CodingKeys: String, CodingKey {
        case name, sex
    }

    enum Husband: String, CodingKey {
        case spouse = "husband"
    }

    enum Wife: String, CodingKey {
        case spouse = "wife"
    }

    init(from decoder: Decoder) throws {
        let values = try decoder.container(keyedBy: CodingKeys.self)

        name = try values.decode(String.self, forKey: CodingKeys.name)
        sex = try values.decode(String.self, forKey: CodingKeys.sex)

        // Creating CodingKey Decoder Container for Husband and wife
        let husbandValue = try decoder.container(keyedBy: Husband.self)
        let wifeValue = try decoder.container(keyedBy: Wife.self)

        // If the husband container able to decode the "husband name" it will be set to spouse
        if let husbandName = try? husbandValue.decode(String.self, forKey: Husband.spouse) {
            spouse = husbandName
        } else {
            // If the wife container able to decode the "wife name" it will be set to spouse
            spouse = try! wifeValue.decode(String.self, forKey: Wife.spouse)
        }
    }
}

Now We can decode the JSON using the below code


let jsonData = """
[
  {
    "name": "Russ Abbot",
    "sex": "Male",
    "wife": "Selena Gomez"
  },
  {
    "name": "Demi Lovato",
    "sex": "Male",
    "husband": "Zak Abel"
  },
  {
    "name": "Toss bot",
    "sex": "Male",
    "wife": "Lovato Gomez"
  },
  {
    "name": "Demi Lovato",
    "sex": "Male",
    "husband": "Sak Tom"
  }
]
""".data(using: .utf8)!

let profile = try? JSONDecoder().decode(Profile.self, from: jsonData)

profile?.forEach { print($0) }

OutPut:

The Result is now very clean and does not contain unnecessary property.

ProfileElement(name: "Russ Abbot", sex: "Male", spouse: "Selena Gomez")
ProfileElement(name: "Demi Lovato", sex: "Male", spouse: "Zak Abel")
ProfileElement(name: "Toss bot", sex: "Male", spouse: "Lovato Gomez")
ProfileElement(name: "Demi Lovato", sex: "Male", spouse: "Sak Tom")

Example 2

Lets take this JSON

[{
"swifter": {
   "fullName": "Federico Zanetello",
   "id": 123456,
   "twitter": "http://twitter.com/zntfdr"
  }
},
{ "bool": true },
{ "bool": false },
{
"swifter": {
   "fullName": "Federico Zanetello",
   "id": 123456,
   "twitter": "http://twitter.com/zntfdr"
  }
}]

According to Quicktype, it generates this code

typealias SwifterOrBool1 = [SwifterOrBool1Element]

struct SwifterOrBool1Element: Codable {
    let swifter1: Swifter1?
    let bool: Bool?

    enum CodingKeys: String, CodingKey {
        case swifter1 = "swifter1"
        case bool = "bool"
    }
}

struct Swifter1: Codable {
    let fullName: String
    let id: Int
    let twitter: String

    enum CodingKeys: String, CodingKey {
        case fullName = "fullName"
        case id = "id"
        case twitter = "twitter"
    }
}

Output

As we can see the key "swifter1" or "bool" is also included with one of the values being nil when JSON data don't include them.

SwifterOrBool1Element(swifter1: Optional(__lldb_expr_78.Swifter1(fullName: "Federico Zanetello", id: 123456, twitter: "http://twitter.com/zntfdr")), bool: nil)
SwifterOrBool1Element(swifter1: nil, bool: Optional(true))
SwifterOrBool1Element(swifter1: nil, bool: Optional(false))
SwifterOrBool1Element(swifter1: Optional(__lldb_expr_78.Swifter1(fullName: "Federico Zanetello", id: 123456, twitter: "http://twitter.com/zntfdr")), bool: nil)

Now with Method init(from decoder: Decoder) we can take over the control on what to include while decoding


struct Swifter: Decodable {
    let fullName: String
    let id: Int
    let twitter: URL
}

enum SwifterOrBool: Decodable {
    case swifter(Swifter)
    case bool(Bool)
}

extension SwifterOrBool {
    enum CodingKeys: String, CodingKey {
        case swifter, bool
    }

    init(from decoder: Decoder) throws {
        let container = try decoder.container(keyedBy: CodingKeys.self)
        if let swifter = try container.decodeIfPresent(Swifter.self, forKey: .swifter) {
            self = .swifter(swifter)
        } else {
            self = .bool(try container.decode(Bool.self, forKey: .bool))
        }
    }
}

We can Decode the JSON Data with the following code.


let json = """
[{
"swifter": {
   "fullName": "Federico Zanetello",
   "id": 123456,
   "twitter": "http://twitter.com/zntfdr"
  }
},
{ "bool": true },
{ "bool": false },
{
"swifter": {
   "fullName": "Federico Zanetello",
   "id": 123456,
   "twitter": "http://twitter.com/zntfdr"
  }
}]
""".data(using: .utf8)! // our native (JSON) data
let myEnumArray = try JSONDecoder().decode([SwifterOrBool].self, from: json) // decoding our data

myEnumArray.forEach { print($0) } // decoded!

Output

As we can see the key "swifter" or "bool" are no longer in the output when, when JSON data don't include them. the result is way cleaner.

swifter(__lldb_expr_80.Swifter(fullName: "Federico Zanetello", id: 123456, twitter: http://twitter.com/zntfdr))
bool(true)
bool(false)
swifter(__lldb_expr_80.Swifter(fullName: "Federico Zanetello", id: 123456, twitter: http://twitter.com/zntfdr))
schani commented 6 years ago

Ok, I can see why you would need this. We have plans for quicktype to handle all of the examples you give, but you might not want to wait that long.

This is relatively to implement, unless there are some special cases I can't think of. Would you be willing to contribute? We're happy to help and answer all your questions.

alexandreos commented 4 years ago

I also need this for transforming data from JSON to Swift, for example: There are some APIs that provide a "currency" amount as String in JSON, but I'd like to map it to a NSDecimalNumber in my Swift code. I have implemented transformers to do so, but if I use quicktype to generate my models I'd need to edit the generated files to add this type of conversion, which defeats the purpose of using a code generator.

We could either have a more flexible template for Swift or use the handlebars approach (discussed in https://github.com/quicktype/quicktype/issues/223), which would make it even more customizable, since anyone would be able to easily create their own templates