Range conform to Codable

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)
    }
}
2 Likes

I had filed a radar for this: rdar://problem/36460457 Conform Range to Codable when Bound conforms to Codable

Swift JIRA seems down, I'll clone the bug to Swift JIRA when the website comes back online.

This feels less like a bug and more like a new feature though, so is JIRA the right place for this?

I think so since JIRA is also used to track enhancements and new features. IIRC there is even a "StartProposal" label.

Sorry I should clarify. I thought JIRA was only for bugs and features that were accepted.

But if there is a "Start Proposal" option in JIRA then great.

https://bugs.swift.org/browse/SR-8649

2 Likes

Updated the original post with more implementation details and a link to the PR.

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).

Thanks!

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.

Thoughts?

1 Like

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.

Due to not finding anything else I've created the PR for the proposal as requested by @Ben_Cohen here: SR-8649: Amendment to [SE-0167] to include Range types by dlbuckley · Pull Request #914 · apple/swift-evolution · GitHub

@dlbuckley Are you sure the partial ranges need unique coding keys?

Reusing the coding keys (as below) won't result in off-by-one errors.

Type lowerBound upperBound
Range "from" "upTo"
ClosedRange "from" "through"
PartialRangeFrom "from" "toEndFrom"
PartialRangeThrough "through" "fromStartThrough"
PartialRangeUpTo "upTo" "fromStartUpTo"
2 Likes

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.

Could the missing bound of a partial range be encoded as null?

Do all data formats support a null value? I see that plists have to use a "$null" string.

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.

Although choosing an upperBound key ("upTo" or "through") for PartialRangeFrom might be tricky.

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.

1 Like

Hi @dlbuckley,

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.

@Ben_Cohen, what do you think?

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.

3 Likes