Skip to content

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

Open
oryan-block wants to merge 2 commits into
masterfrom
bugfix/479
Open

oryan-block wants to merge 2 commits into
masterfrom
bugfix/479

Conversation

@oryan-block

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.

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. findInputValueTypeInType only knows about name/getName methods and public fields, so a strict mode 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. So #422 stays open. The second complaint in #224 (syntax error line numbers when files are concatenated) isn't fixed here either. schemaString() is still parsed as one document without a source name. That's #392. For #465, mapping two dictionary names to ObjectNode still throws "value already present". That's how the BiMap works, but nested inputs under ObjectNode no longer need a dictionary entry, so I'll close that one by hand with a comment. 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.

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>
@sonarqubecloud

Copy link
Copy Markdown

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