Handling Field-Level Permissions in API Responses

I would like the API user to receive responses from the fields they have access to, and an error response for the rest. In the case of a directive, the entire query will return an error, even if the user has access to some fields. I know it is possible to implement the @include logic on the client side to skip certain fields in specific cases, but I would like to avoid this as it requires a lot of work (having to pass information to the client about what they have access to).

I’m considering a different solution:

Would it be correct to use a union on a single field? Something like the example below:

{
  id {
    ... on Value {
      value
    }
    ... on PermissionError {
      message
    }
  }
}

Response when admin:

{
  id {
    value: "my_id"
  }
}

Response for other users:

{
  id {
    message: "You don't have permission to access this field"
  }
}

I know it is possible to implement the @include logic on the client side to skip certain fields in specific cases, but I would like to avoid this as it requires a lot of work (having to pass information to the client about what they have access to).

Yes, you can, but it’s not recommended since the API should handle user access validation instead of relying on arguments.

Would it be correct to use a union on a single field?

Yes, it is correct and and effective approach to handle success and error responses.

If you want a more expressive schema, you can create a directive to validate user access and use a union for field type (my favorite approach to handle user access).

Something like that:

directive @auth(role: UserRole) on FIELD_DEFINITION | OBJECT | INTERFACE

enum UserRole {
  DEFAULT
  PREMIUM
}

type Mutation {
  updatePost(data: UpdatePostInput!): UpdatePostResponse! @auth(role: PREMIUM)
}

union UpdatePostResponse = Post | UnauthorizedError | GenericError 

interface Error {
  message: String!
}

type UnauthorizedError implements Error {
  message: String!
}

type GenericError implements Error {
  message: String!
}