[Pitch] Conform UUID to LosslessStringConvertible

I have a pitch to make UUID more ergonomic by conforming it to LosslessStringConvertible, which the community has already showed previous interest in (Should UUID conform to LosslessStringConvertible?)

Introduction

Make UUID conform to LosslessStringConvertible so it can be created from and converted to a canonical string representation in a round-trip-safe way.

Motivation

UUID already supports conversion via uuidString and init?(uuidString:), but it doesn’t conform to LosslessStringConvertible. This limits its usefulness in generic contexts, especially in frameworks, parsers, and UI code, where developers expect such types to work like Int, Double, or URL.

Because of this, many frameworks (like Vapor) add the conformance themselves. I believe this behavior should be standardized in Foundation.

Proposed solution

I propose conforming UUID to LosslessStringConvertible with the following implementation:

extension UUID: LosslessStringConvertible {
    public init?(_ description: String) {
        self.init(uuidString: description)
    }

    public var description: String {
        self.uuidString
    }
}

This makes UUIDs more convenient to use in SwiftUI, command-line tools, and any parsing or serialization context. Invalid strings still return nil, preserving safety.

Example

let uuid = UUID("E621E1F8-C36C-495A-93FC-0C247A3E6E5F")
print(uuid?.description)
// Output: "E621E1F8-C36C-495A-93FC-0C247A3E6E5F"

View full proposal at [Proposal] Conform UUID to LosslessStringConvertible by jevonmao · Pull Request #1317 · swiftlang/swift-foundation · GitHub

11 Likes

Any thoughts on the issue raised previously regarding lowercase versus uppercase?

2 Likes

Seems reasonable to me.

It seems that the description property of UUID already returns uppercase strings.

The existing init(uuidString:) accepts both uppercase and lowercase strings.

I think it would be make the proposed init?(_ description: String) much more ergonomic if it accepted both uppercase and lowercase UUID strings, and the proposed implementation does so.

I don't know if that goes against the spirit of the LosslessStringConvertible protocol. Its primary purpose seems to be round-tripping between the description value and creating an instance from that string.

The documentation for the protocol says the description must be lossless and be able to create an instance of the type:

The description property of a conforming type must be a value-preserving representation of the original value. As such, it should be possible to re-create an instance from its string representation.

But it doesn't seem to preclude some other string being able to represent a Lossless String Convertible type:

Lossless String Convertible

A type that can be represented as a string in a lossless, unambiguous way.

And the documentation for the init method itself is very open-ended:

init?(_ description: String)
Instantiates an instance of the conforming type from a string representation

So, it seems to me the new init method should accept both uppercase and lowercase versions of the UUID string, just as the existing init(uuidString:) does.

2 Likes

:thought_balloon:
swiftlang/swift-foundation#85 refers to RFC 4122, but it was obsoleted by RFC 9562, which seems to allow lowercased, uppercased, or mixed textual representation; iiuc.

1 Like

That is the issue I'm referring to. RFC 4122 (though now superseded) contains language that the string output should specifically be lowercase, while input should be case-insensitive. Apparently some users have been bitten by the UUID type not producing output of the specified case and my question (though technically separable) is whether in the course of making this stronger guarantee about the string representation of this type we ought to address the reported issue.

3 Likes

Float conforms to LosslessStringConvertible, and also has multiple accepted string representations of the same value:

  • 1.0
  • 1
  • 1.000
6 Likes

I think LosslessStringConvertible specifically allows for multiple accepted string representations when initialising a value from String, so UUID.init?(_ description: String) should allow any mixture of the hexadecimal characters a-f, A-F, and 0-9 (separated by hyphens) that form a valid UUID.

But given that RFC 9562 no longer recommends any case representation over the others, and the long history of Foundation.UUID representing UUIDs as uppercased strings, I think it would be safest to stick to var description: String always returning an uppercased UUID string, not lowercased.

6 Likes

Thank you for clarifying the issue, I was not aware of that.

Yes RFC 9562 seems pretty clear on this:

When in use with URNs or as text in applications, any given UUID should be represented by the "hex-and-dash" string format consisting of multiple groups of uppercase or lowercase alphanumeric hexadecimal characters separated by single dashes/hyphens.

and

Note that the alphabetic characters may be all uppercase, all lowercase, or mixed case

So it seems the UUID description string now meets the latest UUID RFC, but probably should be documented that it returns the uppercased version (It currently gives an example string which is uppercased, so it can be inferred, but maybe it could be more explicit). And lowercased() can be used to get the lowercase version, if needed.

All said, I think this proposal should go ahead, it seems like a small but useful addition.

3 Likes

Of course, since lowercase, uppercase, and mixed case are all explicitly acceptable per the newest RFC, an alternative approach which would promote robust user code would be not to guarantee any particular case for the output—and for even more robustness, to generate mixed-case output that changes randomly per execution (much like hash values).

1 Like

That sounds like it would just generate lots of spurious diffs.

3 Likes

It could, yes, where users are depending on the current non-guaranteed behavior—and I'm not seriously thinking that it would be practically implementable. I think it's still important to kick the tires on a variety of solutions in the design space and explicitly enumerate their trade-offs.

To that specific point: If (supposing) part of the motivation for RFC 9562 relaxing string representation requirements is that actual implementations differ (haha) widely, then spurious diffs are just going to happen unless (1) your output is always coming from a single implementation where its format stability is guaranteed; and/or (2) you always normalize case before output. A UUID type that always returns a mixed-case description would actually have the salutary effect of immediately causing spurious diffs on testing, leading case-sensitive use cases to call lowercased() or uppercased(). This is not the case currently where Hyrum's law is in effect.

2 Likes

You mentioned URL here, which doesn't conform to LosslessStringConvertible, and its description isn't suitable for parsing (if there's a non-nil baseURL).

let a = URL(string: "https://www.swift.org/")!
let b = URL(string: "blog/", relativeTo: a)!

a.description  //->          "https://www.swift.org/"
b.description  //-> "blog/ -- https://www.swift.org/"

Or if the URL is very long. Found that one out the hard way ><

5 Likes

Thank you all for the feedback. I'd like to move this into an abbreviated review and come back to accept it at June 10 2025 if there are no more open issues.

1 Like