[Review] SF-0041: UUID Version Support and Other Enhancements

Hello community,

We are seeking feedback on UUID Version Support and Other Enhancements through 2026-05-27.

Your feedback is an important part of the Swift-Foundation evolution process. All feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to me as the review manager by DM. When contacting the review manager directly, please include the proposal name in the subject line.

Things to consider

The goal of this feedback period is to improve the proposal through constructive discussion. Here are some questions to consider in your feedback:

  • How much effort did you put into your evaluation of this proposal?
  • If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?

More information about Swift-Foundation evolution process is available at

swift-foundation/CONTRIBUTING.md at main · swiftlang/swift-foundation · GitHub

Thank you,

Tina, Review Manager

3 Likes
@available(FoundationPreview 6.4, *)
extension UUID {
    /// The variant of a UUID, as defined by RFC 9562 Section 4.1.
    public enum Variant: Sendable, Hashable {
        /// NCS backward compatibility (variant bits `0xx`).
        case ncs

        /// The variant specified by RFC 9562 (variant bits `10x`).
        case rfc9562

        /// Microsoft backward compatibility (variant bits `110`).
        case microsoft

        /// Reserved for future use (variant bits `111`).
        case reserved
    }

    /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562.
    public var version: Int { get set }

    /// The variant of this UUID, derived from the variant bits (bits 64–65) as defined by RFC 9562.
    public var variant: Variant { get }
}

I had a few questions here before about public enum types on ABI stable packages:

I think an alternative might be a public struct Variant with static var properties exposing ncs etc. The actual enum type could then remain private. Any more pros and cons here that are worth discussing?

Speaking in general about non-frozen public enums. We could use a public struct, but people are still likely to resort to switching over the raw value or producing a series of if statements. We may as well rely on the "unknown default" Swift syntax for the same operation.

I think it's useful to use the public struct approach when it feels like there is a good chance of adding new values and public enum approach when it feels like there is a low chance of adding new values, and frozen public enum when there is literally no possibility of new values.

Given that there is only one reserved value here for future compatibility (we could add a new name if we needed to for that value), and the other bits seem reserved for backward compatibility, this one is either low or no possibility in my book.

4 Likes

I'd make version a frozen enum with 16 values, v1, v2, ... and give it the corresponding Int based raw value.

That's mathematically complete, but it doesn't really have a grounding in the spec which only defines these 4 values.

[edit to clarify: we are discussing the Variant enum, not the version of the UUID]

Got it, no interruption intended, but my comment was indeed about the version property.

I think it becomes more intuitive if there is a id.version Enum in the initializers like this:

public init(_ version: UUID.Version = .4) -> UUID {
  return switch version {
    // ...
    case .4: // return v4 UUID
    // ...
    case .7 // return v7 UUID
    // ...
  }
}

This approach should be backwards compatible through the .4 default value.

It also simplifies the calling of other versions:

Current UUID calls:

        // v4 UUIDs have RFC 9562 variant
        let uuid = UUID()
        #expect(uuid.variant == .rfc9562)

        // v7 UUIDs have RFC 9562 variant
        let v7 = UUID.version7()
        #expect(v7.variant == .rfc9562)

With UUID.Version Enum in the initializer:

        // v4 UUIDs have RFC 9562 variant
        let uuid = UUID() // implicitly .v4
        #expect(uuid.variant == .rfc9562)

        // v7 UUIDs have RFC 9562 variant
        let v7 = UUID(.7) // explicit .v7
        #expect(v7.variant == .rfc9562)

Should there be a version4(using generator: inout some RandomNumberGenerator) method to match the version7 method with the generator parameter?

We could do this, but it would be identical to the existing method:

@available(FoundationPreview 6.3, *)
public static func random(
    using generator: inout some RandomNumberGenerator
) -> UUID

I don't think it is typical for us to have identical methods that do the same thing. We could deprecate, but as I talked about in the pitch here, I think it is a good idea to avoid deprecations just for naming reasons.

2 Likes

I think it could be worthwhile to do a soft deprecation (set the deprecation version to 10000.0) so that the API surface is consistent. That would result in the random method appearing deprecated in documentation and autocomplete, but would not introduce any diagnostics in existing call sites. Notably, a quick GitHub search suggests this method is very rarely used.

Overall, I am supportive of this proposal, and I think adding support for more UUID versions and variants will make the existing UUID type useful in more contexts. I only have two comments around the variant and version representation.

/// Reserved for future use (variant bits `111`).
case reserved

I continue to think calling this case reserved is odd. While it is currently reserved in the RFC a future RFC might define it and give it a name. I could see two solution to this:

  1. Remove the case and mark the enum as @nonexhaustive.
  2. Make Variant struct with a raw value and define static values for the different variants

Version

Why are we using an enum for the Variant but an Int for the Version. Similar, to the variant the version has 8 well defined versions but it can only ever have 16 possible versions due to the layout definition.

As a reference the most used Rust crate for UUID's defines both variant and version as non exhaustive enums.

1 Like

I don't think using the name reserved is a major issue, especially since it can be cleanly renamed or deprecated in later API versions if that slot becomes defined in the future.

For example:

// API v1:
    case reserved3 = 3
// App v1:
    switch variant {
        ...
        case .reserved3: break
    }

Then later:

// API v2:
    case newName = 3

    @available(*, deprecated, renamed: "newName")
    static let reserved = Variant.reserved3
// App v1 compiled against API v2:
    switch variant {
        ...
        case .reserved: break // deprecated warning
    }

That seems like a fairly smooth migration path with good source compatibility.

I'd also prefer Version to be an enum for consistency. Personally, I'd make both enums exhaustive/frozen and explicitly define all 2^3 and 2^4 possible cases respectively.

"Consistency" isn't an end in itself. Int is, by its nature, nonexhaustive for this purpose; integers are straightforward to store, compare, order, etc.; and they obviate the code generation inefficiencies that (at least in earlier versions of Swift) affect enums. Unless there are non-numeric UUID "versions" that we expect to exist in future, Int seems utterly correct and idiomatic here.

BTW, what'd happen on uuid.version = 42, I guess a trap, right?

Int isn’t very self-documenting or pedagogical however. With an enum there is a natural place to document which version is which, and it is easy to jump to that place and learn about the different versions from anywhere that a version is used. Personally, I’d much prefer that to an Int.

(Int also implies that this is something you might want to treat as a number. But you’re never going to do arithmetic with the version. The only reasonable thing you can do is switch over it.)

1 Like

I disagree that Int is utterly correct here. Both variant and version have a fixed amount of possible values defined by the layout of UUIDs in the RFC. Using an Int indicates that all of Ints possible values are correct variants or versions.

I think that's a good point. If we make the enums @nonexhaustive we could always deprecate and add a new case with the updated name.

3 Likes

I think it's more straightforward to think of the numbered versions as... numbers, instead of enumeration cases. x.version == 7 is totally reasonable.

I don’t see how that is in any meaningful way different from x.version == .version7? If you wanted to make the case that these really are numbers, and not just variants with numerical names, a better example would be x.version >= 7. When would that be useful?

I think it’s alright. Compared to using an enumeration, the integer version just needs to define what happens when the version is set to an arbitrary value (version is settable):

  1. Only the lower 4 (or, currently, 3) bits are used; all higher bits are silently ignored.
  2. A precondition failure (or similar trap) occurs if the value is outside the allowed range.
  3. The value is checked silently, and if it falls outside the allowed range, no action is taken.
  4. Any of the above, but with an additional corresponding warning logged to the console.

(edited for clarity)

It's the lower 4 bits that are used.

    /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562.
    public var version: Int {
        get {
            Int(_storage.span.bytes[6] >> 4)
        }
        set {
            var s = _storage.mutableSpan
            var b = s.mutableBytes
            b[6] = (b[6] & 0x0F) | (UInt8(newValue & 0x0F) << 4)
        }
    }

All of the values are defined by the spec here, although half of them are reserved. We could enumerate them, but the result would be 2 versions that this type fully supports, 7 indistinguishably reserved values, 1 other reserved value, 1 unused value, and then 5 versions which somehow are enumerated but we don't have corresponding inits, etc.

This seems overly complicated versus just storing and representing it as an integer.