Repository navigation
Check input object classes against the schema with Jackson - #857
Open
oryan-block wants to merge 4 commits into
Open
oryan-block wants to merge 4 commits into
oryan-block wants to merge 4 commits into
Conversation
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>
A misspelled property on an input class, like `technolgies` for `technologies` in #77, only showed up once a client sent the field: Jackson then failed to convert the argument, or dropped the value if the ObjectMapper ignores unknown properties. The scanner's getter lookup can't tell, since Jackson also maps setters, creators, naming strategies, aliases and any setters. Now the SchemaParser asks Jackson. For each resolver argument whose type is an input object, it looks up the deserializer that the field's own ObjectMapper converts the argument with, and checks that it has a property for every field of the built input object type, through lists, arrays, Optionals and nested input objects. A field it doesn't have is read into a placeholder object with unknown properties made to fail, so ignored names, any setters and ignoreUnknown count as mapped without running the class's constructors or setters. - New SchemaParserOptions.unmappedInputFields: WARN by default, FAIL throws one SchemaError listing every field, IGNORE skips the check - MethodFieldResolver keeps its ObjectMapper, so the check and the conversion use the same one - The scanner's "Cannot find a public getter or field" warning is now debug, since it fired for setter-only classes and similar Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A List, array or Optional of a class with @JsonTypeInfo got the base class's deserializer from the container, so the type id and every field that only a subclass has were reported as unmapped, which failed the build in FAIL mode for inputs that convert fine. The check now looks up the elements' type info the way Jackson does when it creates the container's deserializer, from the property's annotation or else from the class or default typing, and skips them like a polymorphic property. - An input type used by fields whose ObjectMappers handle unknown properties differently is reported once for each consequence, so a field that fails isn't hidden behind one that drops the value - Errors such as NoClassDefFoundError while looking up a deserializer are logged at debug like exceptions, since the lookup used to run on the first query and mustn't fail a build that used to succeed - Tests for Map<String, bean> arguments and polymorphic properties, and contains assertions that show the actual message Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



Fixes #422
Checklist
Description
Stacked on #842, merge that first.
A property name on an input class that doesn't match the schema isn't caught when the schema is built. In the #77 comment that #422 links to,
ProjectInputhastechnologies: [TechnologyInput!]in the schema butprivate List<TechnologyInput> technolgiesin the class. Before #842 that failed the build with the misleading "Expected type 'TechnologyInput' to be a GraphQLInputType" error. With #842 it builds, and then fails when a client sends the field, with Jackson'sUnrecognized field "technologies". A typo on a scalar field never failed the build at all. Either way you find out at runtime, or not at all if theObjectMapperignores unknown properties and the value is silently dropped.The only check today is the scanner's
findInputValueTypeInType, which looks for aname/getNamemethod or a public field. That's fine for finding types, but as a strictness check it's wrong in both directions. It flags setter-only classes,isXgetters,@JsonCreatorand@ConstructorPropertiesclasses, naming strategies,@JsonAliasand@JsonAnySetter, all of which Jackson maps fine. That's why #842 left #422 out. The thing that decides whether a field is mapped is the field's ownObjectMapper, and that's per field (PerFieldObjectMapperProvider) while an input type can be shared by several fields.So I ask Jackson. The new internal
UnmappedInputFieldCheckerruns per resolver field, afterSchemaParserhas built the input objects and wired their directives:convertValuewould use, from that field's mapper.MethodFieldResolvernow creates itsObjectMapperonce, lazily, asinternal val objectMapper, so the argument conversion and the check use the same one, along withgetMethodParameterTypeand the same argument index offsets.extend inputfields are included, and follows lists, arrays,Optionals and nested inputs.FAIL_ON_UNKNOWN_PROPERTIESforced on. It only counts as unmapped if that throws anUnrecognizedPropertyExceptionfor that exact name. So anything Jackson maps or deliberately ignores (setters, creators, naming strategies, aliases,@JsonIgnoreProperties, any setters,ignoreUnknown) isn't reported.Map-like types, delegating creators, builders, custom deserializers, and polymorphic values (type info on the property, the class, or from default typing, including elements of lists, arrays andOptionals).VirtualMachineError, is logged at DEBUG and the argument is skipped, so the check can't break a build that worked before.The message names the field, the class, the usage, what happens at runtime, the properties Jackson knows about, and the fix:
The consequence depends on the mapper and the field. It "fails whenever the field is sent", "fails for every 'X' value sent" when the field has a default (graphql-java fills it in, so it's always sent), or "drops the field's value" when the mapper ignores unknown properties. If one type is used by fields whose mappers disagree, each consequence is reported once.
The new
SchemaParserOptions.unmappedInputFieldstakes a new public enumUnmappedInputFields:IGNORE,WARNorFAIL. I madeWARNthe default. The issue asks for an error, but an error by default would break every schema that has a deliberately unmapped field, and there's no way to tell those apart from typos.WARNlogs one line per mismatch.FAILthrows a singleSchemaErrorlisting all of them.IGNOREdoesn't create the checker at all. Since the check now covers this properly, #842's "Cannot find a public getter or field" WARN inSchemaClassScanneris now DEBUG, because it's the one that gave false positives for setter-only classes and the like.What isn't checked:
Map,JsonNode,Objectand type-variable parameters, custom deserializers, delegating creators, builders (only their nested inputs), polymorphic values (skipped, not checked per subtype), classes with@JacksonInject, unknown fields of classes with@JsonUnwrappedproperties, and directive arguments. Type-variable parameters already fail at runtime with aClassCastExceptioninshouldValueBeConverted. That's an existing bug I didn't touch, and since the check uses the samegetMethodParameterType, fixing it later fixes both.Expect small textual conflicts with #843 in
SchemaParser.ktand with #849 inSchemaParserOptions.kt.The new
UnmappedInputFieldsTest(with Java fixtures inUnmappedInputFieldsFixtures.java) captures log output and runs real queries next to the schema build, so each report is backed by what actually happens at runtime. Reported cases: the #77technolgiestypo inWARNandFAILmode (exact text, and the mutation fails), a scalar typo, a KotlinisActivefor a schemaactive, typos two levels deep throughList, array andOptionalunder anOptionalargument, a type shared by two fields plus anextend inputfield, a mapper that ignores unknown properties, aSNAKE_CASEmapper on one field only, a mismatched field with a default value, mappers with different consequences for one type, and argument index offsets (GraphQLResolversource,DataFetchingEnvironment, suspend continuation). Not reported, inFAILmode with every value round-tripping in a query: the Java patterns listed above plus@JsonIgnoreand@JsonUnwrapped, Kotlin data classes,@JsonNaming, case-insensitive properties, property-level@JsonIgnoreProperties, recursive inputs,Map,Map<String, bean>andObjectNodeparameters, delegating, builder, polymorphic and custom-deserializer classes, polymorphic list andOptionalelements, and a module throwingNoClassDefFoundErrorduring the lookup. The known gaps above have tests that pin the current behaviour. I wrote the first four tests before the fix, against existing API only, and all four fail onbugfix/479: the #77 and scalar andisActivecases log nothing useful, and the Java patterns test gets the scanner's false-positive WARN. The rest use the new option, so they don't compile on the base.mvn clean verifypasses.Behaviour change: with the default
WARN, schemas build as before but each input field the field's mapper can't map logs one WARN fromgraphql.kickstart.tools.UnmappedInputFieldChecker, including scalar fields that used to get no message. Deliberately unmapped fields can go in@JsonIgnorePropertieson the class, or the check can be turned off withunmappedInputFields(UnmappedInputFields.IGNORE). Three existing fixtures in this repo now log it, and all three really do fail at runtime. #842's scanner WARN is now DEBUG with shorter text. Jackson deserializers for resolver argument types are now built when the schema is built instead of on the first query, so startup is a bit slower and the first query a bit faster. In a synthetic benchmark that was about 0.1 ms and 33 KB per field with a nested Kotlin input and the default per-field provider, and nothing measurable with one shared mapper. I haven't measured it on a large real schema yet. The per-fieldObjectMapperis now created once perMethodFieldResolver, so if oneSchemaParserbuilds the schema twice, both share it, whereprovide()used to be called again.SchemaParserOptionsgains a property, so the data class'scopy()gets another parameter and there's a newcomponent17(). That's binary-incompatible for compiled callers ofcopy(), like whenallowUndeclaredDirectiveswas added, so this should go out in a minor version. Worth a note in the release notes, along with theIGNOREopt-out.🤖 Generated with Claude Code