Skip to content

Instantly share code, notes, and snippets.

@nicklockwood
Created December 31, 2022 18:56
Show Gist options
  • Star 11 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nicklockwood/ff51117ac8139248507ecc84a1ed7fed to your computer and use it in GitHub Desktop.
Save nicklockwood/ff51117ac8139248507ecc84a1ed7fed to your computer and use it in GitHub Desktop.
PolymorphicCoding.swift
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