Range is a useful tool in Swift but I was surprised to find that it doesn't conform to Codable. These days it doesn't take much to add conformance but it feels like it should be part of the standard library.
Is there a reason it doesn't conform to Codable yet or is it simply one of those things that we haven't got around to adding in support for yet? I haven't written up a formal proposal yet, but here is the conformance I propose:
extension ClosedRange: Codable where Bound: Codable {
private enum CodingKeys: String, CodingKey {
case lowerBound = "from"
case upperBound = "through"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let lowerBound = try container.decode(Bound.self, forKey: .lowerBound)
let upperBound = try container.decode(Bound.self, forKey: .upperBound)
guard lowerBound <= upperBound else {
throw DecodingError.dataCorruptedError(
forKey: CodingKeys.upperBound,
in: container,
debugDescription: "upperBound (through) cannot be less than lowerBound (from)"
)
}
self = lowerBound...upperBound
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.lowerBound, forKey: .lowerBound)
try container.encode(self.upperBound, forKey: .upperBound)
}
}
extension Range: Codable where Bound: Codable {
private enum CodingKeys: String, CodingKey {
case lowerBound = "from"
case upperBound = "upTo"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
let lowerBound = try container.decode(Bound.self, forKey: .lowerBound)
let upperBound = try container.decode(Bound.self, forKey: .upperBound)
guard lowerBound <= upperBound else {
throw DecodingError.dataCorruptedError(
forKey: CodingKeys.upperBound,
in: container,
debugDescription: "upperBound (upTo) cannot be less than lowerBound (from)"
)
}
self = lowerBound..<upperBound
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.lowerBound, forKey: .lowerBound)
try container.encode(self.upperBound, forKey: .upperBound)
}
}
extension PartialRangeUpTo: Codable where Bound: Codable {
private enum CodingKeys: String, CodingKey {
case upperBound = "fromStartUpTo"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self = try (..<container.decode(Bound.self, forKey: .upperBound))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.upperBound, forKey: .upperBound)
}
}
extension PartialRangeThrough: Codable where Bound: Codable {
private enum CodingKeys: String, CodingKey {
case upperBound = "fromStartThrough"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self = try (...container.decode(Bound.self, forKey: .upperBound))
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.upperBound, forKey: .upperBound)
}
}
extension PartialRangeFrom: Codable where Bound: Codable {
private enum CodingKeys: String, CodingKey {
case lowerBound = "toEndFrom"
}
public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
self = try container.decode(Bound.self, forKey: .lowerBound)...
}
public func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.lowerBound, forKey: .lowerBound)
}
}
It's OK to raise Jiras for "obviously" useful additions like this, where they'd need an evolution proposal but any disagreement would be about the how, not whether, of the implementation. Even for bigger projects like SortedSet. We do need to draw a line at debatable features, but that's a pretty subjective line.
In this case, while it is an addition to the interface, I think it can be considered an omission and done as an amendment to the original codable proposal.
Could you raise a PR against that proposal to add it? The core team can then discuss and decide how to handle it (either accept straight out, or run a mini-review).
Sure I can make a proposal amendment, but it appears as though it needs its own section as Range doesn't come under a Foundation type, instead it's part of the standard library. With that in mind I do wonder if there are any other 'low hanging fruit' in the standard library that would also benefit from a bit of Codable love.
I don't have any thoughts on further types at the moment (happy to help audit if necessary), but I think collecting more similar low-hanging fruit is an excellent idea and will make for an even more compelling push to adopt Codable throughout.
I've been digging through the standard library this morning and have yet to come across anything else that looks like a good candidate for Codable conformance. Things like errors and characters don't really make sense, and tuples where it might be useful (but probably recommended against) but aren't possible due to them being an unnamed type.
I have had a quick look through Swift Foundation too, and the only discrepancy with the 167 proposal I can see is actually that the Data type has conformance where it isn't stated in the proposal. In my quick look I can't see any other types that need conformance, so it appears the range types are the only ones that slipped under the radar here.
While true for the partial ranges, I believe that it's better to be more verbose with the keys to make it obvious when looking at it from a human reading JSON perspective rather than from simply a computer encoding/decoding the value perspective.
I would add to this that it's also useful to be able to tell the difference between missing/corrupted data and a different data type. {"from": ...} could indicate either a PartialRangeFrom, or a corrupted Range/ClosedRange. It's nice for symmetry, but the benefits of easily spotting invalid data might be worth departing from it.
This is another option, and might be a nice compromise. Yes, all formats are required so support nil representations as part of the Encoder/Decoder requirements (making whatever adjustments they have to, like ”$null”), so this should be possible to do.
In theory it wouldn't matter which one you chose as it would be ignored. But from a clarity and predictability point of view it's not great. If you were presented with a JSON that you had to parse and didn't know anything about the code that generated it, it's not as obvious which range type would be which. Where as using the keys I propose make it much more obvious.
I think it's a great idea to conform Range and friends to Codable, but I'm not sure about the process to do so. Amending an already-approved proposal to add more features doesn't seem 100% correct to me.
We've followed that process in the past for similar things (like adding new std lib collapsings to the conditional conformance SE). Will come back with an official core-team position shortly.