Skip to content

Proposal: Resolving the "Double-Time" Sync Debt in Spring for GraphQL #1406

@xenoterracide

Description

@xenoterracide

8

The Problem: The Synchronization Tax

Spring for GraphQL’s SDL-First mandate forces a manual "Double-Entry" loop. This creates a fragmented development experience where third-party generators (DGS) are disconnected from the Request Execution Flow.

1. The Resolver Contradiction (The "Non-Null" Trap)

In GraphQL, a field can be Non-Null in the schema but resolved via a separate @schemamapping.

Example Schema:

type User {
  id: ID!
  username: String!
  reputationScore: Int!  # Non-Null Schema Field
}
  • The Conflict: DGS generates "record User(String id, String username, Integer reputationScore)". Because it’s Int!, the Record enforces a non-null reputationScore in the constructor.
  • The Breakdown: If reputationScore is resolved via a separate service call, the User object must be instantiated WITHOUT that score first to be passed to the field resolver. The strict Record makes this impossible.
  • The Validation Sacrifice: You are forced to make the DTO field @nullable just to allow instantiation. Now, you’ve lost the ability to use Jakarta to validate that the data-backed fields (id, username) are present, because the DTO is now a "leaky" bucket.
@Controller
public class UserController {

    // The "Partial" DTO hack required because the generator-produced 
    // Record is too strict for the execution flow.
    public record UserPartial(String id, String username) {}

    @QueryMapping
    public UserPartial userById(@Argument String id) {
        return new UserPartial(id, "JavaEnthusiast");
    }

    @SchemaMapping(typeName = "User", field = "reputationScore")
    public int getReputationScore(UserPartial parent) {
        // This is called lazily. We use the parent ID to fetch the score.
        return reputationService.getScoreForUser(parent.id());
    }
}

2. The Validation Pipeline Gap

DGS-generated Records trigger Objects.requireNonNull in the constructor, causing a hard NPE BEFORE Jakarta Validation can run.

  • The Result: The framework cannot collect multiple ConstraintViolations because instantiation fails immediately on the first null.
  • The "Apathy" Loop: To get Jakarta validation, developers must use "soft" POJOs with nulls, defeating the purpose of strict Java Records.

The Solution: Decoupling Validation from Instantiation

Path A: Standards-Based Code-First (MicroProfile)

Supporting MicroProfile GraphQL makes Java Records the "Source of Truth."

  • Pre-Instantiation Validation: Validates raw input against the schema BEFORE the constructor is called.
  • Cohesive Validation: Allows for strict Records that are only instantiated once the entire input set is deemed valid.

Path B: A "Smart" Binding-Aware Generator

Alternatively, a generator designed for the Spring/Jakarta flow:

  • Contextual Nullability: Recognizes that "Non-Null" in SDL doesn't always mean "Non-Null" in the initial DTO if a resolver is present.
  • Partial Mapping Hooks: Generates models that support @schemamapping without requiring a second, manual DTO.

Conclusion

Spring should offer a native way to decouple input validation from instantiation, supporting strict Java Records without breaking the API contract or forcing "Double DTO" hacks.


Reference

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions