Skip to content

Schema mapping is called, but returned (mapped) object is possibly not applied to result #1111

@TOTHT0MI

Description

@TOTHT0MI

Context

Spring Version: 3.4.1
Spring Cloud: 2024.0.0
Kotlin Version: 2.0.21
Kotlin coroutines: 1.10.1
Whole dependency list
  • org.springframework.boot:spring-boot-starter-graphql
  • com.graphql-java:graphql-java-extended-scalars:20.2
  • org.springframework.data:spring-data-commons
  • org.springframework.cloud:spring-cloud-starter-loadbalancer
  • org.springframework.boot:spring-boot-starter-oauth2-client
  • org.springframework.security:spring-security-oauth2-client
  • org.keycloak:keycloak-admin-client
  • org.springframework.boot:spring-boot-starter-security
  • io.micrometer:micrometer-tracing
  • io.zipkin.reporter2:zipkin-reporter-brave
  • org.springframework.boot:spring-boot-starter-actuator
  • io.micrometer:micrometer-tracing-bridge-brave
  • com.fasterxml.jackson.module:jackson-module-kotlin
  • io.github.wimdeblauwe:error-handling-spring-boot-starter
  • org.springframework.boot:spring-boot-starter-oauth2-resource-server
  • org.springframework.cloud:spring-cloud-starter-netflix-eureka-client
  • io.github.thibaultmeyer:cuid:2.0.3
  • org.springframework.boot:spring-boot-starter-webflux
  • org.jetbrains.kotlinx:kotlinx-coroutines-core
  • org.jetbrains.kotlinx:kotlinx-coroutines-reactor
  • org.springframework.cloud:spring-cloud-starter-circuitbreaker-reactor-resilience4j
  • io.projectreactor.kotlin:reactor-kotlin-extensions
  • org.springframework:spring-aspects
  • com.github.ben-manes.caffeine:caffeine:3.1.8

Description

I have the following schema (only relevant things are put here)

type Subscription {
    id: CUID!
    owner: User!
    name: String!
}
type User {
    id: UUID!
    username: String!
    firstName: String!
    lastName: String!
    email: String!
    locale: String!
}

scalar CUID
subscription(id: CUID!): Subscription

I have the following controller and DTOs:

data class SubscriptionDTO(
    val id: CUID,
    val ownerId: UUID,
    val name: String,
    val limits: SubscriptionLimitsDTO,
    val paymentInfo: PaymentInfoDTO,
    val active: Boolean,
    val deleteWarningSent: Boolean
)
data class UserDTO(
    val id: UUID,
    val username: String,
    val firstName: String,
    val lastName: String,
    val email: String,
    val locale: String
)

@QueryMapping
suspend fun subscription(
    @Argument("id") id: CUID,
    principal: JwtAuthenticationToken
): SubscriptionDTO {
    logger.info("Called sub")
    logger.debug("Returning subscription of id {}", id.toString())
    return authorityManager.withSubscriptionAuthenticationContext(
        Permission.READ_SUBSCRIPTION,
        id,
        principal
    ) {
        it.subscription
    }
}

@SchemaMapping(typeName = "Subscription", field = "owner")
fun owner(subscription: SubscriptionDTO): UserDTO {
    logger.info("Called 1")
    val result = keycloakService.getUser(subscription.ownerId).orElse(null) ?: UserDTO(
        id = UUID.randomUUID(),
        username = "unknown",
        firstName = "Unknown",
        lastName = "Unknown",
        email = "[email protected]",
        locale = "en"
    )
    logger.info(
        "Resolving owner ({}) to UserDTO. Result is: {}",
        subscription.ownerId,
        result.toString()
    )
    return result
}

And this is my scalar config:

@Configuration
class GraphQLConverters {

    @Bean
    fun graphqlSourceBuilderCustomizer(): RuntimeWiringConfigurer {
        return RuntimeWiringConfigurer { wiringBuilder ->
            wiringBuilder
                .scalar(ExtendedScalars.UUID) // Register custom UUID scalar
                .scalar(cuidScalar()) // Register custom CUID scalar
        }
    }

    // Define CUID Scalar
    private fun cuidScalar() = GraphQLScalarType.newScalar()
        .name("CUID")
        .description("Custom CUID scalar")
        .coercing(object : Coercing<CUID, String> {
            override fun serialize(dataFetcherResult: Any): String {
                return (dataFetcherResult as CUID).toString()
            }

            override fun parseValue(input: Any): CUID {
                return CUID.fromString(input.toString())
            }

            override fun parseLiteral(input: Any): CUID {
                if (input is StringValue) {
                    return CUID.fromString(input.value)
                }
                throw CoercingParseLiteralException("Invalid value for CUID")
            }
        })
        .build()
}

My test query is the following (yes the subscription exists)

query {
  subscription(id: "cy9sw9aebqj734knmj3kic6i") {
    id
    name
    owner {
      id
      username
    }
  }
}

Howerver I get an error: The field at path '/subscription/owner/id' was declared as a non null type, but the code involved in retrieving data has wrongly returned a null value. The graphql specification requires that the parent field be set to null, or if that is non nullable that it bubble up null to its parent and so on. The non-nullable type is 'UUID' within parent type 'User'

I figured the problem is because the SubscriptionDTO is returned with the owner being UUID (so no child fields), and the Schema Mapping value is ignored.
However I can see from the logs, that the mapper is indeed called:

2025-01-13T16:40:27.690Z  INFO 1 --- [graphql-service] [or-http-epoll-3] [678541fbd50a9698e543be612d92e520-4f11b1c7df9aeb05] h.g.g.c.SubscriptionController           : Called sub

2025-01-13T16:40:27.729Z  INFO 1 --- [graphql-service] [or-http-epoll-1] [678541fbd50a9698e543be612d92e520-cd679ba828bfd6a3] h.g.g.c.SubscriptionController           : Called 1 

2025-01-13T16:40:27.762Z  INFO 1 --- [graphql-service] [or-http-epoll-1] [678541fbd50a9698e543be612d92e520-cd679ba828bfd6a3] h.g.g.c.SubscriptionController           : Resolving owner (f2b762ec-1b9e-4145-ab5b-a634e2b32491) to UserDTO. Result is: UserDTO(id=f2b762ec-1b9e-4145-ab5b-a634e2b32491, [email protected], firstName=Test, lastName=Test, [email protected], locale=hu)

I don't know if you need any further logs or information, if so I can provide it in a reply. However I've tried many ideas, but none of them worked, so I'm reaching out here, because this is possibly a bug.

Metadata

Metadata

Assignees

No one assigned

    Labels

    status: invalidAn issue that we don't feel is valid

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions