Skip to content

Register input field types that have no Java property - #842

Merged
oryan-block merged 6 commits into
masterfrom
bugfix/479
Oct 17, 2026
Merged

oryan-block merged 6 commits into
masterfrom
bugfix/479

Conversation

@oryan-block

@oryan-block oryan-block commented Oct 10, 2026 •

Copy link
Copy Markdown
Collaborator

Fixes #479
Fixes #355
Fixes #224

Checklist

  • Pull requests follows the contribution guide
  • New or modified functionality is covered by tests

Description

If the scanner can't find a Java property for an input field, it never registers that field's type. So a nested input type or custom scalar that's only reachable through that field is never built, and the schema build fails with this:

Expected type 'InsuranceAgreementCreateOneWithoutInsuranceAgreement_IncludedAgreementInput' to be a GraphQLInputType, but it wasn't! Was a type only permitted for object types incorrectly used as an input type, or vice-versa?

There are a few ways to end up there. The input can be a Map or ObjectNode resolver parameter, which has no properties at all. The getter can fail to match the schema field, like getIncludedAgreement_Back() for IncludedAgreement_back in #479. Or the field can be private and mapped with @JsonProperty. #355 is the same problem with a custom scalar: foo: Foo on a CreateHumanInput bound to a Map. The workaround people use is a dummy query that takes every nested input as an argument, just so the scanner reaches them. And the message sends you looking for an output type used as an input, which is the complaint in #224.

The InputObjectTypeDefinition branch of SchemaClassScanner.handleNewType just logged "Cannot find definition for field ..." when findInputValueType came back empty, and moved on. SchemaParser.determineInputType only builds types the scanner observed, so it threw.

Now the input branch lives in handleInputObjectFields, which takes a nullable class so it can recurse. When a field has no property, handleInputValueWithoutJavaType looks at the field's type. A nested input object is registered without a class and its fields are scanned the same way. That only happens the first time it's seen, so recursive inputs are fine. If a class for it turns up later through another field, setTypeIfMissing fills it in and it's scanned as usual, without a "Two different classes" error. The triage prototype used java.util.Map as a stand-in class instead, but input types never go into the class BiMap anyway, and Map's own methods could then get matched as properties. A custom scalar goes through handleFoundScalarType. An enum can't be built without its class. So if none is found anywhere by the end of the scan, it fails with a SchemaClassScannerError that names the enum, the input type, the field and the class, and suggests a public getter or field, or a dictionary entry. Undefined types and output types are left for the parser to report.

Two smaller scanner fixes came out of review. Fields of a Map or JsonNode input are no longer matched against methods declared by the JDK or Jackson, so an ObjectNode input with a field named values, elements or fields works. Getters that a Map subclass declares itself are still used. Also, a dictionary class for an input field is now matched against the unwrapped type. So a list field whose element class comes from the dictionary no longer fails with "Java class is not a List".

On the parser side, both throws in determineType/determineInputType now go through unexpectedTypeError, which tells three cases apart: the type isn't defined in the schema, it's the wrong kind (an object, interface or union used as an input, or an input object used as an output), or it's defined but the scanner never reached it. For that last case the hint depends on the kind of type. For example, includeUnusedTypes doesn't help for input types or scalars. The message names the field or argument that uses the type, plus the line, column and file name when known:

Type 'Filter' used by argument 'filter' of field 'Query.name' at line 2, column 18 is an object type, which can't be used as an input type. Input types are input objects, enums and scalars.

A file name shows up as in UnreachedType.graphqls, same as the syntax errors from #838.

To find that usage I search the definitions for the same TypeName instance, and only on the error path. Passing the context down through determineType/determineInputType would've been the other option, but the fix for #321 rewrites their List/NonNull branches in a separate PR and I didn't want the two to conflict.

The new NestedInputTypesTest runs real queries, not just the schema build. It covers a recursive RequestFilter through a Map parameter, two ObjectNode inputs sharing a nested input, the #479 case (mismatched getter and a private @JsonProperty field, two levels deep), a class found after the input was first reached without one, the dictionary list field, ObjectNode field names and Map subclass getters. SchemaClassScannerTest runs the #355 schema with a mutation. SchemaParserTest checks the exact message for each case, plus the enum errors. The main scenarios (Map, ObjectNode, mismatched getter, the #355 scalar) fail on master with the old error, and the message tests fail on the old text. Two of the tests are guards that already passed: a class found later, and an enum class found elsewhere.

#422 asks for a build-time check that input class property names match the schema. I left it out of this one. findInputValueTypeInType only knows about name/getName methods and public fields, so a strict mode built on it would flag setter-only classes, isX getters, @JsonCreator classes, naming strategies and @JsonAnySetter. Doing it right means asking Jackson, but the ObjectMapper is per field (PerFieldObjectMapperProvider) while an input type can be shared by several fields. That's in #857. The second complaint in #224 (syntax error line numbers when files are concatenated) isn't fixed here. #838 already fixed it on master (#392). For #465, mapping two dictionary names to ObjectNode still throws "value already present" here. That's #851. One more limit: a custom scalar inside a Map input reaches the resolver as whatever Jackson converts it to, not as the scalar's Java object. That's #436, fixed in #856.

The closed duplicates #448 (of #355) and #266, #261 and #326 (of #479) build and run with this change.

Behaviour change: the old "Expected type ... to be a GraphQLInputType" text is gone, so anything matching on it won't match anymore. An enum that's only reachable through a field without a property now fails during scanning with a SchemaClassScannerError instead of in the parser. The "Cannot find definition for field" WARN is replaced by "Cannot find a public getter or field for field ...". The new WARN is only logged for real input classes, so Map and JsonNode inputs don't log it anymore. Input objects reached this way no longer show up in getUnusedDefinitions(). Also, a misspelled property on an input class whose nested input type is only reachable through it used to fail the build, although with the misleading error. Now it builds with the WARN and fails when a client sends that field, or the value is dropped if the ObjectMapper ignores unknown properties. Misspelled scalar properties already behaved that way. Probably worth a note in the release notes.

🤖 Generated with Claude Code

oryan-block and others added 2 commits October 9, 2026 21:34
When the scanner couldn't find a Java property for an input field, e.g.
because the input is bound to a Map or ObjectNode, the getter name
doesn't match the schema field (getFoo_Back for foo_back) or the field
is private, it only logged "Cannot find definition for field ..." and
never registered the field's type. Nested input objects and custom
scalars only reachable that way were left out of the scan, and the
parser then failed with "Expected type 'X' to be a GraphQLInputType,
but it wasn't!", which pointed users at the wrong problem.

The scanner now still registers what such a field needs. A nested input
object is registered without a class and its fields are scanned the
same way; a class found for it later is set and scanned as usual, so it
doesn't clash. A custom scalar is registered like any other. An enum
can't be built without its Java class, so if none is found for it
anywhere, the scan fails with a message naming the enum, the input type,
the field and the class, and how to fix it. The garbled warning is
replaced by one that's only logged when an input class (not a Map or
JsonNode) lacks a matching property.

Input fields of a Map or JsonNode are no longer matched against the
methods and fields it inherits from the JDK or Jackson, so fields named
like values, elements or fields work with ObjectNode. Getters declared
by a Map subclass are still used. A class taken from the dictionary for
an input field is matched against the field's unwrapped type, so a list
field whose element type is in the dictionary no longer fails with "Java
class is not a List".

A misspelled property on an input class whose nested input type is only
reachable through it used to fail the build with the misleading error
above. It now builds with the warning and, like any other misspelled
property, fails when a client sends the field, or the value is dropped
if the ObjectMapper ignores unknown properties. There's still no
build-time check that input class properties match the schema (#422).

The parser's "Expected type ... to be a GraphQLInputType" error is
replaced by one that tells apart a type that isn't defined, a type of
the wrong kind (an output type used as an input or vice versa) and a
type that was defined but never reached while scanning, with a hint for
reaching it that depends on the kind of type. It names the field or
argument using the type and its source location. Schemas passed through
schemaString() are still parsed as one document without a source name,
so their line numbers count from the start of all strings (#392).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
oryan-block and others added 4 commits October 17, 2026 09:21
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The directive enum test from #852 expected the parser's "Expected enum
with name 'Purpose' but found none!" for an enum that's also used by an
input field bound to a Map. With this branch the scanner already fails
for that enum, since no class can be found for it, and names the input
type, the field and how to fix it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Fix the two compiler warnings for the java.util.Map and Object class
  literals in isPropertyMap
- Flatten handleInputObjectFields with early returns
- Give the three enum-without-class messages the same fix hint, so only
  the reason differs
- Pick the type kind in unexpectedTypeError without a triple negation
- Write the source name as "in <file>", like the syntax errors from #838
- Drop "javax." from the library class prefixes, nothing there is an
  input class
- Shorten the missing property warning
- Fold the ObjectNode method name test and the Map subclass enum error
  test into their neighbours

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@sonarqubecloud

Copy link
Copy Markdown

@oryan-block
oryan-block merged commit 418b804 into master Oct 17, 2026
7 checks passed
@oryan-block
oryan-block deleted the bugfix/479 branch October 17, 2026 15:38
oryan-block added a commit that referenced this pull request Oct 17, 2026
…puts

Since #842, a scalar used only in a Map input is registered, so the Map test no longer needs another input to reach its scalars. The typed cases share one schema and resolver, and the UUID scalar comes from the end-to-end fixtures.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant