# A dangling foreign key, a non-null field, and the whole response going null `schema.sql` deliberately puts no foreign key constraint on `book.author_id` (see docs/01), which makes it possible to insert a `Book` whose `authorId` matches no row in `AUTHOR` at all — a data integrity problem that a real foreign key would normally prevent, but one that's entirely possible in systems that don't enforce referential integrity at the database level, or during a migration, or from a bad import. Both test classes run the same experiment against it: ```java Author real = authorRepository.save(new Author("Ursula K. Le Guin")); bookRepository.save(new Book("The Dispossessed", real.getId())); bookRepository.save(new Book("Orphan Book With No Author Row", 9_999_999L)); ``` Two books, one with a valid author, one with an `authorId` (`9999999`) that doesn't exist. Then the same query as docs/02 and docs/03: ```graphql { books { title author { name } } } ``` ## What happens with the naive resolver `NaiveAuthorResolver.author()` runs `authorRepository.findById(9_999_999L).orElse(null)` for the orphan book and gets back `null`. The schema says `author: Author!` — non-null. GraphQL Java's response, captured verbatim from the real HTTP response body (`c_danglingAuthorIdNullsTheEntireBooksListViaNonNullPropagation`): ```json {"errors":[{"message":"The field at path '/books[1]/author' 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 'Author' within parent type 'Book'","path":["books",1,"author"],"extensions":{"classification":"NullValueInNonNullableField"}},"data":null} ``` Source: [docs/output/naive-c-dangling-foreign-key-null-propagation.txt](output/naive-c-dangling-foreign-key-null-propagation.txt) Read that error message once, slowly, because it names its own mechanism: a non-null field that resolves to null doesn't just fail *that* field — the null has to go somewhere, and the GraphQL specification's rule is that it propagates upward to the nearest field in the response tree that *is* allowed to be null. Here, the chain is `author` (non-null) inside `Book` (non-null element of a non-null list, `[Book!]!`) inside `books` (non-null). There is no nullable field anywhere between the failure and the root, so the null keeps climbing until it reaches the top: the entire `data` key becomes `null`, and the one broken book takes the four working ones down with it. `errors` is populated, `data` is not partial — it's entirely absent. ## What happens with the batched resolver `BatchedAuthorResolver.author()` builds its result map from `authorRepository.findAllById(...)`, and the orphan book's `authorId` simply never appears as a key in that map — the resolver's own doc comment calls this out explicitly: a missing map entry, not an explicit null value stored against the key. The response, from the same experiment run against the `batched` profile: ```json {"errors":[{"message":"The field at path '/books[1]/author' 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 'Author' within parent type 'Book'","path":["books",1,"author"],"extensions":{"classification":"NullValueInNonNullableField"}},"data":null} ``` Source: [docs/output/batched-c-dangling-foreign-key-null-propagation.txt](output/batched-c-dangling-foreign-key-null-propagation.txt) Byte-for-byte the same error, same path, same classification. That's the point of running the experiment against both profiles rather than just one: Spring GraphQL treats "no entry in the `Map` returned from a `@BatchMapping` method" as identical to "no `Author` found from a per-object `@SchemaMapping` resolver" — the null-propagation behavior lives entirely in GraphQL Java's execution engine, not in either resolver strategy, and switching from naive to batched buys better query counts without changing this failure mode at all. ## Why this matters more than it looks like it does Non-null propagation is a deliberate, spec-mandated safety property — it exists so a client can trust that a non-null field, if present, is never actually null, which simplifies every consumer of the API. But it means a single bad row, one dangling foreign key in one book out of however many, is enough to null out an entire query's `data`, including books whose own data was completely fine. In a production schema, the practical response to this is usually one of: make the field nullable if "we don't know the author" is a legitimate state; enforce the foreign key at the database layer so the dangling reference can't be created in the first place; or have the resolver return a sentinel "unknown author" object instead of `null`/a missing map entry. This module does none of those on purpose, specifically so the failure mode is visible and reproducible rather than designed around.