Best approach for Codable on Types I don't own

If I don't want to violate "don't conform types you don't own to protocols you don't own", what's a good solution?

For example, let's take CGPoint. Say I want to encode/decode it in a different JSON format.

  • I could create a MyPoint type, and use it instead, but I'd really rather not have to update the entire codebase.

  • I could instead add an extension on CGPoint that returns a MyPoint and encode/decode that right before sending. But that would move the encode/decoding responsability for MyPoint to user:


struct MyPoint: Codable { ... }

extension CGPoint {
      var encodePoint: MyPoint { .... }
}

struct SomeOtherThing: Codable {
     let point: CGPoint
  
     // must manually implement init(decoder) and encode() so that i can convert the CGPoint to MyPoint
}

Also, this means I can't do JSONEncoder().encode(CGPoint( ... )) I have to remember to use the other format.

I realize what I'm asking for may be going against the grain a bit, but just wanted to see if you guys had any better solutions to this scenario regardless.

2 Likes

What if you add a custom static encode/decode method to the extension?

I think it still shifts the responsibility of the custom encoding & decoding to the enclosing type SomeOtherThing in the example above would have implement init(decoder) and encode() if it had a CGPoint property.

You shouldn't have to update the whole codebase.

The only things that absolutely must be updated are the types that currently store CGPoint and that are Codable. Those types will need to be refactored to actually store a MyPoint, and then to expose getters/setters that can turn that MyPoint into a CGPoint and back again. The MyPoint can be an artefact only of the storage.

If you are willing to go further and write custom Codable implementations (which you'll have to do for MyPoint at least anyway), you can even avoid storing a MyPoint at all: just create it in your implementation of init(from:) and encode(to:). Then the rest of your codebase can continue to store and work on CGPoints, and only the types that need to be able to serialise themselves have to worry about how it's done.

1 Like

A nice way to use the @lukasa idea is to use property wrappers. That way you have to write only one word to wrap the var in MyPoint(I called it EncodablePoint), and the rest of the app will still see the CGPoint

@propertyWrapper
struct EncodablePoint: Encodable {
    var wrappedValue: CGPoint
    enum CodingKeys: CodingKey {
        case x
        case y
    }
    func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        try c.encode(wrappedValue.x, forKey: .x)
        try c.encode(wrappedValue.y, forKey: .y)
    }
}

usage:

struct Foo: Encodable {
    @EncodablePoint var pt: CGPoint = CGPoint(x: 123, y: 456)
    var other = "zxcv"
}

let foo = Foo()
let encoded = try! JSONEncoder().encode(foo)
print(String(bytes: encoded, encoding: .utf8) ?? "nil") // {"pt":{"x":123,"y":456},"other":"zxcv"}
print(type(of: foo.pt)) // CGPoint
16 Likes

Right, that’s actually the approach I suggest in my initial post. But the question I posed was if there was a better way, a way where I don’t have to provide a custom Codable implementation to every type that uses CGPoint, because a) it’s nice and less error prone to get the synthesized implementation and b) the user has to remember that CGPoint has to be converted to MyPoint before encoding.

Nice! I like that you don’t have to implement Codable on struct Foo here

I think the best outcome is the one that @cukr proposed, which is also the first property wrapper I've seen that I think is really genuinely useful. 10/10 great work!

2 Likes