Created
December 31, 2022 18:56
-
-
Save nicklockwood/ff51117ac8139248507ecc84a1ed7fed to your computer and use it in GitHub Desktop.
PolymorphicCoding.swift
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import Foundation | |
// Here's a pretty typical scenario where you want to encode a polymorphic type - | |
// in this case a Shape type that can be either a Square or Circle. Swift provides | |
// a nice pattern for doing this in a type-safe way using enums: | |
struct Circle: Codable { | |
var radius: Int | |
} | |
struct Square: Codable { | |
var width: Int | |
var height: Int | |
} | |
enum Shape { | |
case circle(Circle) | |
case square(Square) | |
} | |
// The problem is (or rather used to be) that there was no automatic synthesis of | |
// Codable support, but this was solved in a recent Swift update. | |
// The problem now is that the default Codable implementation produced butt-ugly | |
// JSON that looks like this: {"circle":{"_0":{"radius":5}}} | |
// In many cases that doesn't matter, but if having a beautiful output format is | |
// something you care about, then read on. | |
// What we actually want is something more like: {"type":"circle","radius":5} | |
// To achieve that, we need to have a way to represent the case type in the | |
// serialized data. We'll start by defining a second enum to represent the type: | |
enum ShapeType: String, Codable { | |
case circle, square | |
} | |
// Next we need to write the Codable implementation. Previously I would have | |
// done this by adding a read-only type field to each concrete shape type | |
// with a hard-coded value for the type, so that the type would be serialized | |
// along with the other fields, but it turns out there's a much more elegant | |
// solution. | |
// When encoding or decoding an object, you can actually open *multiple* | |
// containers and use them to read or write to the same output context. That | |
// means that we can first write the output dictionary for the concrete shape | |
// struct using its synthesized encoder, and then add an additional type key | |
// to the output using a second container: | |
extension Shape: Codable { | |
private enum CodingKeys: CodingKey { | |
case type | |
} | |
var type: ShapeType { | |
switch self { | |
case .circle: return .circle | |
case .square: return .square | |
} | |
} | |
init(from decoder: Decoder) throws { | |
let a = try decoder.container(keyedBy: CodingKeys.self) | |
let b = try decoder.singleValueContainer() | |
switch try a.decode(ShapeType.self, forKey: .type) { | |
case .circle: self = try .circle(b.decode(Circle.self)) | |
case .square: self = try .square(b.decode(Square.self)) | |
} | |
} | |
func encode(to encoder: Encoder) throws { | |
var a = encoder.singleValueContainer() | |
switch self { | |
case let .circle(value as Encodable), | |
let .square(value as Encodable): | |
try a.encode(value) | |
} | |
var b = encoder.container(keyedBy: CodingKeys.self) | |
try b.encode(type, forKey: .type) | |
} | |
} | |
// And here's the proof that it works | |
let shape = Shape.circle(Circle(radius: 5)) | |
let data = try! JSONEncoder().encode(shape) | |
let json = String(data: data, encoding: .utf8)! | |
print(json) // {"type":"circle","radius":5} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment