Attached macros on enum case declarations

I recently had a situation where being able to attach peer macros to enum case declarations would've been a boon towards removing custom initializers. Something like this:

@APIResponsePayload
enum MyResponsePayload {
    @Attributes(statusCode: 200)
    case success(MyPayload)

    @Attributes(statusCode: 404)
    case notFound
}

expanding to this:

enum MyResponsePayload: APIResponsePayload {
    case success(MyPayload)
    case notFound

    init(from decoder: any Decoder) throws {
        guard let urlResponse = decoder.userInfo[Self.urlResponseUserInfoKey] as? HTTPURLResponse else {
            throw APIResponsePayloadDecoderError.notHTTPURLResponse
        }
        
        switch urlResponse.statusCode {
        case 200:
            self = .success(try .init(from: decoder))
        case 404:
            self = .notFound
        default:
            throw APIResponsePayloadDecoderError.unknownResponse
        }
    }
}

This unfortunately doesn't seem possible today as enum case declarations don't support attached macros. Is there a specific reason why this was omitted, or is this just work that hasn't been prioritized yet?

2 Likes

Have you considered attaching values in the payload?

enum Foo {
    case success(Data, status: Int = 200)
    case failure(status: Int = 404)
}

print(Foo.success(Data()))              // success(0 bytes, 200)
print(Foo.success(Data(), status: 201)) // success(0 bytes, 201)
print(Foo.failure())                    // failure(404)
print(Foo.failure(status: 405))         // failure(405)

let v: Foo = Foo.failure()
switch v {
    case .failure(let code): print(code) // 404
    default: print("ok")
}

This doesn’t really fit the use-case we’re going after, which is attaching metadata to individual enum cases for use in other methods (e.g. decoding). In my specific use-case, I’m trying to streamline the mapping of HTTP status codes (and maybe other attributes, like the Content-Type header) to the appropriate type-safe enum case.

For example, longer term, I’d love to be able to version a /user endpoint and have this be the extent of the additional parsing code:

@APIResponsePayload
enum UserAPIResponsePayload {
    @Attributes(statusCode: 200, contentType: "application/vnd.my-app.user.v1")
    case successV1(UserPayloadV1)

    @Attributes(statusCode: 200, contentType: "application/vnd.my-app.user.v2")
    case successV2(UserPayloadV2)

    @Attributes(statusCode: 404)
    case notFound
}

Here, these attached attributes are used to select the case (based on response metadata like status code/headers/other); they’re not part of the case’s associated values.

Beyond my own use-case, this kind of per-case metadata could be useful for things like analytics tagging, routing hints, and other custom synthesis behaviors. The common theme is attaching structured, compile-time metadata to individual enum cases for use elsewhere; not modeling additional runtime data in the payload itself.

This does not align with my experience.

You can declare @Attributes in your example as "marker" peer macro which expands to nothing, and generate all the code you need in your @APIResponsePayload implementation.

You… are correct. :face_with_raised_eyebrow:

I don’t recall what errors I ran into when I first tried this several months ago, but it may have just been user error, as spinning up a fresh example works as expected now.

This thread can be closed.