0

I have this json string:

let json = """
    {
        "name": "Wendy Carlos",
        "hexA": "7AE147AF",
        "hexB": "851EB851"
    }
"""
let data = Data(json.utf8)

...which I'd like to encode to (and back from) this struct, using Codable:

struct CodeMe: Codable {
  var name: String
  var hexA: Int
  var hexB: Int
    
  enum CodingKeys: String, CodingKey {
    case name, hexA, hexB
  }
}
let encoder = JSONEncoder()
let decoder = JSONDecoder()

But hexA and hexB are Strings (in the JSON) and I need these to be Ints in the Swift object. I have already written 20 functions for that purpose. e.g. in pseudocode:

func hexAConversion(from hex: String)->Int {
    // returns an Int between -50 and 50
}

func hexBConversion(from hex: String)->Int {
    // returns an Int between 0 and 360
}

Taking into account that there are a fair few conversion schemes like this and that I need I need write 20 more functions (for the Int->Hexadecimal roundtrip), how would I write a custom decoding and encoding strategy that works with the above?

I've had a look at these solutions: Swift 4 JSON Decodable simplest way to decode type change but my use case seems slightly different, as the accepted answer looks like it deals with a straight type conversion whereas I need to run some functions.

Ribena
  • 1,086
  • 1
  • 11
  • 20
  • Why don't you encode and decode your hexa properties as String? – Leo Dabus Dec 15 '20 at 23:09
  • 1
    Why do you need 20 methods to do that? Would something like this work: `func hexConversion(from hex: String)->Int{return UInt(hex, radix: 16)!}` And `func hexConversion(from myInt: Int)->String{return String(myInt, radix: 16, uppercase: false)}` – Igor Tupitsyn Dec 15 '20 at 23:10
  • @IgorTupitsyn `UInt` initializer returns an unsigned integer `UInt` not an integer `Int`. You probably meant `Int(hex, radix: 16)` – Leo Dabus Dec 15 '20 at 23:20
  • Yep. Looks like it will also work. – Igor Tupitsyn Dec 15 '20 at 23:28
  • It would work to convert from Hexa String to Int but I don't know what OP meant by // returns an Int between 0 and 360 – Leo Dabus Dec 15 '20 at 23:32
  • @IgorTupitsyn if I only wanted to convert from hex to Int and back that would be fine. But As I have it now, my functions do 2 things: 1) convert from hex to Int32 and 2) remap the Int32 to a new value, with 20 different types of remapping (e.g. from 0 to 50, from 0 to 360, from 0 to 50 but only for positive Int32, from -25 to 25, etc.) – Ribena Dec 16 '20 at 01:35
  • To add to that previous comment... and to answer @LeoDabus ... I want hexA and hexB to be Ints because that's what I will need to draw sliders in my UI. But when the user changes a value through the slider, I want to save that back as Hexadecimal in my JSON. – Ribena Dec 16 '20 at 01:46

1 Answers1

1

For Codable encoding and decoding that requires custom transformation type stuff, like that, you just need to implement the initializer and encode methods yourself. In your case it would look something like this. It's a bit verbose just to try to get the idea across really clearly.

init(from decoder: Decoder) throws {
    let container = try decoder.container(keyedBy: CodingKeys.self)
    self.name = try container.decode(String.self, forKey: .name)
    let hexAString: String = try container.decode(String.self, forKey: .hexA)
    self.hexA = hexAConversion(from: hexAString)
    let hexBString: String = try container.decode(String.self, forKey: .hexB)
    self.hexB = hexBConversion(from: hexBString)
}

func encode(to encoder: Encoder) throws {
    var container = encoder.container(keyedBy: CodingKeys.self)
    //Assuming you have another set of methods for converting back to a String for encoding
    try container.encode(self.name, forKey: .name)
    try container.encode(hexAStringConversion(from: self.hexA), forKey: .hexA)
    try container.encode(hexBStringConversion(from: self.hexB), forKey: .hexB)
}
creeperspeak
  • 5,403
  • 1
  • 17
  • 38
  • Works great. What would be the less wordy way of writing this? I have a lot of hexadecimal values that use the same hex decoding scheme, for example. – Ribena Dec 16 '20 at 02:44
  • 1
    @Ribena I have done some similar tasks in my project and to cut down on long function names and whatnot by implementing extensions with `fileprivate` scope since all conversion needs only be done in that one spot. So for example I might make a String extension with computed property called `hexAIntValue`, which would allow you to change the associated decode line to `self.hexA = try container.decode(String.self, forKey: .hexA).hexAIntValue`. This isn't any more correct than what you've done. I just find it to be a little more readable for me, personally. – creeperspeak Dec 16 '20 at 20:13
  • Cool. What I've done for now is similar I guess. I have a bunch of String extensions to deal with the various hex-to-whatevs conversions, and have made your initial solution a one-liner. e.g. `self.portamento = zeroTo50FromHex(from: try container.decode(String.self, forKey: .portamento))`. But I still need to have such a line for each key. I was hoping there might be a way I could coalesce all the zeroTo50FromHex lines together, for example. Some typealiases and Sourcery might otherwise do the trick I imagine but I'm not sure I want to go to that effort, as I've never used Sourcery before. – Ribena Dec 17 '20 at 23:49