Attaches a developer-supplied predicate to a field, an argument, or an input field. The reflected static method takes the surrounding table and the relevant scalar values, and returns a jOOQ Condition. The rewrite folds that Condition into the generated WHERE clause: by default in addition to the implicit column = ? predicates the rewrite would otherwise emit, or replacing them when override: true.

Names from inside the generator. This page names types from graphitron’s own classification model. They are accurate today, and worth knowing when a rejection message quotes one back at you, but they are not part of the authoring contract: the classification walk that produces them is being drained, and these names retire with it. What stays stable is the schema you write, the directive reference that describes it, and the diagnostics glossary.

@condition is the escape hatch for predicates Graphitron can’t infer from the catalog: regex matches, range filters, date arithmetic, joins through correlated subqueries, vendor-specific predicates, multi-column boolean shapes. It also drives the dynamic enclosingOverride cascade at field level: an outer field-level @condition(override: true) suppresses the implicit-column predicates from an input-typed argument, but inner argument-level @condition annotations still fire. (The rewrite preserves inner explicit conditions, the legacy generator dropped them, see filmsOuterOverrideFilterInput in the example schema for the test that pins this divergence.)

SDL signature

directive @condition(
    condition:        ExternalCodeReference
    override:         Boolean = false
    contextArguments: [String!]
) on FIELD_DEFINITION | ARGUMENT_DEFINITION | INPUT_FIELD_DEFINITION

Parameters

Name Type Default Description

condition

ExternalCodeReference

(required in practice)

Points at the static Java method that returns the Condition. className: and method: are both required, the rewrite reflects the method’s parameter list against (in order) the surrounding table, the input or scalar parameters, and any contextArguments. argMapping: rebinds GraphQL argument names to differently-named Java parameters when the conventions diverge.

override

Boolean

false

When true, suppresses the rewrite’s automatic equality predicates for the affected element only: at argument level, the implicit column = ? for that one argument; at field level, the implicit predicates for every direct argument and the column-equality predicates inside input-typed arguments. On a @nodeId leaf whose join route to the target table does not resolve, it also suppresses the route demand itself and hands the method the resolving table plus the leaf’s decoded key. Inner explicit @condition annotations are preserved on either axis.

contextArguments

[String!]

[]

Names of values to pull from the GraphQL GraphQLContext at runtime. Each named context value becomes a Java parameter the reflected method receives, alongside the table and the input arguments. Wire the values into GraphQLContext in your servlet bootstrap.

Canonical examples

Argument-level, no override (the explicit predicate is AND-ed with the implicit column IN (…​)):

type Query {
    citiesByName(
        cityNames: [String!] @field(name: "CITY")
            @condition(condition: {className: "com.example.CityConditions", method: "cityMethod"})
    ): [City]
}

Field-level, no override (the explicit predicate is AND-ed with the implicit predicates for every argument):

type Query {
    cities(
        countryId: String! @field(name: "COUNTRY_ID"),
        cityNames: [String!] @field(name: "CITY")
    ): [City] @condition(condition: {className: "com.example.CityConditions", method: "cityMethod"})
}

Field-level, override: true on a filter input (its fields resolve against film, the query’s return-type table). The outer @condition runs, the implicit column-equality predicates from FilmConditionInput are suppressed, but an inner @condition on FilmConditionInput.filmId still fires:

type Query {
    filmsOuterOverrideFilterInput(filter: FilmConditionInput): [Film!]!
        @condition(
            condition: {
                className: "com.example.conditions.InputFieldConditionFixtures",
                method:    "outerOverrideMethod"
            },
            override: true
        )
}

input FilmConditionInput {
    filmId: Int @field(name: "FILM_ID")
        @condition(condition: {
            className: "com.example.conditions.InputFieldConditionFixtures",
            method:    "filmIdCondition"
        })
}

Net effect: (film_id >= 2) AND (film_id = 1) — both the outer override and the inner explicit @condition are honoured, even though the outer override is true.

Multi-segment input path with a nested input. The two-level nesting lets a leaf-level @condition reference its enclosing-input field by path:

input NestedFilmInput {
    inner: InnerFilmInput
}

input InnerFilmInput {
    filmId: Int @condition(condition: {
        className: "com.example.conditions.PlainFilmIdConditions",
        method:    "filmIdCondition"
    })
}

The rewrite extracts inner.filmId from the outer argument map and passes it to filmIdCondition as the bound parameter.

Context-argument flow. The reflected method receives the table, any explicit input parameters, and the named context values:

type Query {
    citiesForCurrentUser: [City]
        @condition(
            condition:        {className: "com.example.CityConditions", method: "tenantScoped"},
            contextArguments: ["tenantId"]
        )
}

tenantScoped(City table, UUID tenantId) runs at request time with tenantId pulled from GraphQLContext.

FK-target @nodeId filter. When an input field carries both @nodeId(typeName: "X") and @condition, the field decodes an opaque id for a foreign-key-linked table X (reached from the consuming field’s table through a catalog foreign key). The reflected method’s table parameter is then X, not the consumer’s table (soknadsmangeltype here, the return-type table of the query consuming this filter):

input SoknadsmangeltypeFilterInput {
    regelverksamlingId: ID! @nodeId(typeName: "Regelverksamling")
        @condition(condition: {
            className: "com.example.conditions.SoknadsmangeltypeConditions",
            method:    "iRegelverksamling"
        }, override: true)
}

The method signature is iRegelverksamling(Regelverksamling rs, Integer regelverksamlingKey): the second parameter is the decoded key of Regelverksamling, typed by that node type’s key columns, because a @nodeId slot’s value is decoded before it leaves the generated code. The first parameter is an alias for the FK-target Regelverksamling, and the rewrite wraps the returned Condition in a correlated subquery, EXISTS (SELECT 1 FROM regelverksamling rs WHERE soknadsmangeltype.<fk> = rs.<pk> AND <your predicate>), so the predicate runs against X while the outer row set is preserved. The developer method is identical to any other @condition method; it neither knows nor cares that its Condition lands inside an EXISTS rather than the top-level WHERE. Composite-key NodeType targets are supported: the correlation ANDs every foreign-key column pair (EXISTS (SELECT 1 FROM project p WHERE p.org_id = project_note.org_id AND p.project_id = project_note.project_id AND <your predicate>)). The same EXISTS is emitted wherever the predicate lands, including inline child-field fetchers and @splitQuery batched fetchers, not only top-level query conditions.

The EXISTS applies only when the filter field carries a value. It is a semi-join: it keeps just the rows that have a matching row on the far side of the foreign key, so applying it for a value nobody supplied would silently narrow the query from "the whole collection" to "the part of the collection that has the relation at all". An omitted field, an explicit null, and an empty list therefore all leave the query unfiltered by this field, exactly as an implicit filter field with no value does. This holds whatever the method’s signature binds, and when the field carries no value the method is not called: there is no value for it to map, and the wrapper is not yours to opt out of from inside it. If you want a far-side restriction that always applies, write a field-level @condition (it receives the consuming field’s own table) and write the EXISTS yourself in jOOQ, where the semi-join is your SQL rather than the generator’s.

Constraints

  • Applies on FIELD_DEFINITION, ARGUMENT_DEFINITION, and INPUT_FIELD_DEFINITION. The element type fixes the predicate’s scope: argument-level conditions can only see their own argument and any context arguments, field-level conditions see all of the field’s arguments, input-field-level conditions see the input’s enclosing argument map (the rewrite extracts the leaf value via the path from the outer argument).

  • condition.className: and condition.method: are both required for a meaningful directive. A @condition whose reference resolves to no method (missing className: or method:) surfaces as field '<name>' @condition: …​ or argument '<name>' @condition: …​.

  • Overloads of the referenced method name are legal exactly when they agree on the binding shape: the same parameter count, the same static-ness, the same return type, the same throws clause, and, position by position, each position either a jOOQ Table parameter in every declaration or identical in name and declared type in every declaration. Only the table parameters may differ, in name as well as in type. Such a set is one @condition target: the generated call site is typed from the coordinate, so your compiler performs the selection, which is what lets one declaration per participant of a multitable interface or union keep its own generated typed columns. A set that disagrees on anything else is rejected, naming the axis it disagreed on and every signature the generator read. See Filter a multitable interface or union for the three forms and the trade-off a mixed set makes.

  • argMapping: is supported on @condition (unlike on @externalField, @enum, @sourceRow). Use it to rebind a GraphQL argument name to a differently-named Java parameter, the rewrite carries the mapping into the reflected method’s parameter list. The right-hand side may also walk into a nested input field; the form is the same at every directive and is described once in Binding a parameter to a nested input field.

  • contextArguments: values must already be wired into the runtime GraphQLContext map, the rewrite passes the listed keys through unmodified. A typo here surfaces as a null parameter at request time, not as a build error.

  • Field-level override: true suppresses the implicit predicates from the field’s direct arguments and from any input-typed argument nested under them. Inner explicit @condition annotations on the input’s fields are preserved, override does not cascade through them. (Divergence from the legacy generator: legacy semantics dropped them. The rewrite’s behaviour is the intended one and filmsOuterOverrideFilterInput pins it as a regression-fence.)

  • Argument-level override: true is local: it suppresses the implicit column = ? for that one argument, nothing else.

  • An input field carrying both @nodeId(typeName: "X") and @condition binds the condition method’s table parameter to the FK-target table X (the table the decoded id identifies), reached through the catalog foreign key from the consuming field’s table. The predicate is emitted as a correlated EXISTS over that foreign key, so the method receives an alias for X rather than the consumer’s table. Single-column and composite-key NodeType targets are both supported (the composite correlation ANDs every foreign-key column pair). The EXISTS applies only when the field carries a value, an omitted field, an explicit null and an empty list each contributing no predicate; the method is not called in that case. This holds at every emission site: top-level query conditions, inline child-field fetchers, and @splitQuery batched fetchers.

  • On a @nodeId leaf whose route to the target table does not resolve, override: true takes the whole WHERE contribution: the method receives the resolving table (each participant’s own table on a multi-table consumer) plus the leaf’s decoded key. This is the ladder’s second rung: where the route does resolve the FK-target contract above applies unchanged, and override: false plus an unresolvable route still fails the build. A leaf on which some participants route and others do not is rejected: the two contracts hand the method different tables, and nothing downstream would catch that, since a set of per-participant declarations accepts both as readily as a Table<?> parameter does. See Multitable filter inputs.

  • A parameter bound to the whole of a @nodeId slot receives the decoded key, never the wire string, and this holds at every such coordinate: argument or input field, routed or author-owned, override: true or false, and whether the @condition sits on the slot itself or on the enclosing field. An arity-1 node key arrives as the key column’s Java type, a composite key as a jOOQ Row, and a list-shaped slot wraps either in List. Ids that are malformed, or encoded for another node type, fail the request before your method runs, so there is nothing for it to parse or validate. Declaring the parameter as anything but that type fails the build, naming the coordinate and the type it must have. Bind such a parameter by giving it the slot’s name, or with an argMapping: whose right-hand side is the bare slot; the type-based pairing that binds a differently-named parameter reads the slot’s wire type (String for an ID) and will not pair a decoded key against it.

  • A dotted argMapping: path is not a whole-slot binding and gets a key column instead. It is the projection described in Projecting a key column out of a node id, and it resolves whether or not you spell the column. Opening the slot with the column (argMapping: "p: filmId.film_id") names it, and is the way to ask for one column of a composite key. Descending through an input object and stopping on a @nodeId input field (argMapping: "p: filter.languageId", written on a field-level @condition) names nothing past the node id, so the node type’s key decides: one column and that column is the projection, since there is nothing else the binding could mean; two or more and the build fails naming them, one binding carrying one value. Either way the parameter receives a decoded column’s own value and never the encoded id, and its declared Java type must be that column’s, which a primitive cannot be: the projected read is boxed at every path shape, so declaring int rather than Integer is a build error naming the parameter and the type to declare. An array or type-variable parameter and a method compiled without -parameters keep your compiler as their backstop, neither being a shape the build can name. Where the node id is absent, omitted by the client or sent as null anywhere on the projected path, the parameter receives null and your method decides what an absent filter means; a field-level @condition is bound to the whole field, so it is called either way. That is the contrast with the FK-target @nodeId whole-slot rail above, where an absent value contributes no predicate and the method is not called at all.

  • At a composite key the two spellings differ, and the difference is a real limit rather than an oversight. A bare binding gets the whole key as a jOOQ Row; a path dotted to the node id gets the build error above, a projection being per-column by construction with no way to name a Row. Two remedies: name a key column past the node id, or move the @condition onto the @nodeId input field itself, where the whole-slot handoff above applies.

  • Nested input types must be carrier-mapped to flow through @condition. A class-backed input maps to its Java record (and any input above it must also be a Java record); a jOOQ-record-backed input works only at the leaf (jOOQ records can not contain other records). An input is class-backed when the Java parameter it flows into is that record type; leaving it without a carrier routes through the PojoInputType path, which works for scalar paths at any depth.

  • Listed nested input types ([InnerInput] inside an outer record) require a Java record carrier on the outer input, the rewrite needs a stable Java type for the list element.

See also

  • @reference supplies catalog-driven joins that @condition can layer additional filtering on.

  • @field(name: …​) is the column-binding axis, @condition and @field coexist on the same slot, @field drives the implicit predicate, @condition adds (or replaces) the explicit one.

  • An input type’s backing class determines the carrier the condition method reads from: the carrier is the Java class the method parameter is typed as (inferred, not declared); a carrier-free input arrives as the runtime Map traversal’s leaf values.

  • @service is the alternative when a whole fetcher is custom code, @condition is for adjusting a single predicate inside an otherwise generated query.

  • How-to: Stacking and overriding conditions covers the override cascade rules, nested-input record conventions, and the divergence from legacy semantics.