Repository navigation
Conversation
…t names A generated document failed to parse, or failed schema validation, in eight independent ways. A class resolving to no slot closed its body immediately, which GraphQL has no syntax for. A type the schema defined itself was named by a field but never declared. An enumeration drawing its values from an ontology was dropped while fields kept naming it; an enum type defines one or more values (GraphQL spec, October 2021, section 3.9), so such an enumeration is carried as a custom scalar instead. A type whose name landed on that of a class or an enumeration was declared alongside it, although every type of a schema shares one namespace in which a name is unique (section 3.3); such a name is now left undeclared, with a warning. The list of implemented interfaces was joined with a comma rather than an ampersand. A linkml:Any class was declared under its raw name while the fields referring to it used the camelcased one. A mixin carrying mixins of its own stayed an object type although other types implemented it. And the set of linkml:Any names lived on a mutable class attribute shared by every instance, so generating a second schema in the same process truncated it. The reference snapshot of the biolink model in this repository did not parse before this change and parses after it, with no type and no field lost. The docs gain sections on mixins, types, enumerations and fieldless classes, and record the gaps left open: no root operation type, names encoded with the reserved double underscore, interface field covariance, and implements being derived from mixins alone. Signed-off-by: jdsika <carlo.van-driesten@bmw.de>
jdsika
force-pushed
the
fix/graphqlgen-valid-document
branch
from
November 12, 2026 14:51
9179eee to
2946c99
Compare
This was referenced Nov 12, 2026
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.
Summary
gen-graphqlemits a document that is not valid GraphQL. Eight independent defectsare fixed here; each one was reproduced against a minimal schema before being
touched. Section references are to the
GraphQL specification, October 2021.
Syntax Error: Expected Name, found '}'Unknown type 'CityCode'Unknown type 'VehicleKind'TypeError: There can be only one type named 'Person'Syntax Error: Unexpected Name 'RichMixin'ImplementsInterfacesjoins with&linkml:Anyclass was declared under its raw name while fields referring to it used the camelcased onescalar any object→Unexpected Name 'object', andAnyObjectleft undefinedType Consumer must only implement Interface types, it cannot implement Nested.linkml:Anynames lived on a mutable class attribute shared by every instance}The last two interact. Promoting the nested mixin to an interface is not enough on
its own:
Consumer implements Nestedthen has to declareimplements Baseaswell, so the generator now walks the transitive mixin closure. Likewise, a
fieldless mixin becomes
interface BareMixin { _placeholder: Boolean }, andevery type implementing it has to repeat that field, so the placeholder is also
emitted for a class whose interface is in that position.
Evidence that this is not hypothetical: the reference snapshot in this repository,
tests/linkml/test_biolink_model/__snapshots__/biolink.graphql, does not parseon
main. After the fix it parses. It gains 52&corrections, 65_placeholderfields, and declarations for nine types that were named butdangling:
BiologicalSequence,FrequencyValue,IriType,LabelType,NarrativeText,PredicateType,SymbolType,TimeType,Unit.Only a type a field actually names is declared; a name matching a built-in scalar,
or one the document already carries as a class or an enum, is left alone.
docs/generators/graphql.rstgains sections covering mixins, types,enumerations, fieldless classes, and the gaps listed below.
How was this tested?
The pre-existing
test_graphql_validityonly callsparse(), which is why thesedefects shipped. The new tests build the schema and run
validate_schema.Twelve test functions, eighteen cases, were added to
tests/linkml/test_generators/test_graphql.py:test_the_generated_document_is_a_valid_schema, parametrized over seven schemas(empty class, custom type, mixin, nested mixin,
linkml:Any, a type sharing aname with a class and an enum, an enumeration without permissible values)
test_a_class_without_a_slot_still_defines_a_fieldtest_an_implementation_repeats_the_field_of_a_fieldless_interfacetest_interfaces_are_separated_by_an_ampersandtest_a_mixin_of_a_mixin_is_an_interface_and_is_implemented_alongtest_a_type_of_the_schema_itself_is_declared_as_a_custom_scalartest_a_type_no_slot_names_is_not_declaredtest_a_type_named_after_a_built_in_scalar_is_not_redeclaredtest_a_type_sharing_a_name_with_a_class_or_an_enum_is_not_declared_twicetest_an_enum_without_permissible_values_is_declared_as_a_scalartest_an_any_class_is_declared_under_the_name_its_fields_usetest_one_generator_does_not_see_the_any_class_of_anotherRunning the file against the previous generator fails 16 of the 18 cases. The
two exceptions are
test_a_type_named_after_a_built_in_scalar_is_not_redeclaredand the
type-sharing-a-namecase of the validity test, which pass either way byconstruction — the previous generator declared no scalars at all, so it could not
declare a colliding one — and are kept as regression guards on the narrowness of
the new declaration rule.
Commands, with results:
uv run pytest tests/linkml/test_generators/test_graphql.py tests/linkml/test_scripts/test_gen_graphql.py -q→ 29 passeduv run pytest tests/linkml/test_biolink_model/test_biolink_model.py --with-biolink --with-slow -k graphql -q→ 1 passed against the regenerated snapshotuv run pytest tests/linkml/test_generators tests/linkml/test_scripts -q→ 2050 passed, 64 skipped, 6 xfailed, 1 failed. The failure istest_markdowndatadictgen.py::test_datadict_personinfo, anetwork/krokisnapshot stale at metamodel version 1.11.0 against the current 1.12.0. It fails
the same way on
mainand is unrelated to this change.ruff 0.11.13 checkandruff 0.11.13 format --checkon both changed Pythonfiles → clean.
codespell 2.4.1over both plus the.rst→ clean.tests/linkml/test_generators/__snapshots__/kitchen_sink.graphqlis byte forbyte unchanged: it references no schema-defined type, has no fieldless class and
no multi-mixin. That is the reason the rule is "declare the types a field names"
rather than "declare every type in the schema" — the latter was tried first and
added
AgeInYearsTypeandPhoneNumberTypeto that snapshot, which nothingreferences.
Areas of uncertainty
examples/is deliberately untouched. Those artifacts come from theMakefile, not from a test, andpersoninfo.graphqlis already stale atmetamodel version 1.7.0 for reasons unrelated to this change. This change
contributes exactly two
&corrections and threescalardeclarations there;regenerating would bury that in unrelated drift. Happy to do it in a separate
commit if preferred.
packages/linkml_runtime/src/linkml_runtime/linkml_model/graphql/meta.graphqlis also left untouched (linkml_model/graphql/meta.graphql shipped in the linkml-runtime wheel does not parse #67), although it is shipped inside the
linkml-runtimewheel and does not parse today (
Unexpected Name 'Extensible', the commadefect verbatim). It appears to be vendored from
linkml/linkml-modelratherthan generated here, so regenerating it in this PR looked like the wrong place.
Please say if it should be.
graphql-coretolerates
scalar Intand the built-in wins. The guard is a deliberate choicepinned by a test rather than a correctness requirement.
_placeholderfield name is a convention this introduces. It is a legalname and does not collide with the reserved
__prefix, but the spelling isopen to discussion.
generator docs, because each needs a design decision rather than a bug fix:
§3.3.1, so
it is not servable on its own. LinkML already carries
tree_root, but theshape of the generated query fields is a design question.
NameCompatibilityencodes a name that is not alphanumeric throughout with__: it prefixes a digit-leading name with it and wraps any other characterin it, as in
__PLUS_SIGN__, while GraphQL reserves__forintrospection (§2.1.9).
This accounts for the nine remaining biolink errors, five from digit-leading
names and four from character substitution.
NameCompatibilityis used only by this generator, so it is safely separable. A related cosmetic
bug belongs with it:
common/naming.pylogsoutputbefore the encodingis assigned, so the warning reports the unencoded name as "the resulting
encoded name".
slot_usagekeeps the narrowed type,which an interface it implements may disagree with — roughly a hundred
biolink errors of the form
Interface field X.subject expects type NamedThing! but Y.subject is type Behavior!. LinkMLis_ais not mapped toa GraphQL interface at all today, so this is a modelling decision.
implementsis derived frommixinsalone. An abstract class that is nevermixed into another therefore becomes an interface that no object type
implements — twenty of the sixty-two interfaces in the biolink output, two of
them used as field ranges. Conversely, a class listed in another class's
mixinswithout being declaredabstractormixinstays an object type,which the class mixing it in may then not implement; this still reaches
LinkML's own
meta.yaml, although as a type-system error rather than thesyntax error it produced before. Both follow from the same decision as gap 3
and belong with it.
The residual errors in the regenerated biolink snapshot are exactly those gaps:
one missing query root, nine reserved
__names and a hundred covariancemismatches, 110 in total, with no other category.
This change went through an independent adversarial review before being opened,
which is where the third and seventh defects in the table above came from, along
with three corrected specification citations and gap 4. The duplicate-name defect
was found as a regression of the first attempt at the custom-scalar fix: before
it, no scalar was declared at all, so none could collide.
Checklist
AI Assistance
If you used AI tools while preparing this PR, you are still the author and responsible for understanding, verifying, and defending your submission. Please engage with reviewers personally rather than through your agent during feedback and revisions. See our AI Covenant for details.