Skip to content

fix(graphql): align SDL nullability with introspection for nullable annotations - #321

Merged
allmonday merged 1 commit into
masterfrom
fix/issue-320-output-nullability
Oct 6, 2026
Merged

allmonday merged 1 commit into
masterfrom
fix/issue-320-output-nullability

Conversation

@allmonday

Copy link
Copy Markdown
Collaborator

Fixes #320

What changed

1. compose SDL render (defect 1) — _render_struct_type_sdl decided the ! suffix with _is_optional alone, which cannot see a None member inside a Literal (verified: _is_optional(Literal["draft", None]) is False — the Literal branch in _is_optional compares type(None) against the member values, so it never fires). Its sibling _build_type_ref in the same module already handled this via literal_info. Both renderers now agree:

type LiteralTaskDTO {
  """Allowed values: draft (or null)"""
  note: String        # was String! — introspection already rendered SCALAR String
}

2. entity output SDL (defect 2, option A) — map_to_sdl no longer forces NON_NULL on output for nullable annotations (Optional[T] / T | None / Literal[..., None]). Output SDL now matches map_to_introspection and the Python annotation itself:

type TaskLiteEntity {
  ...
  """Allowed values: draft (or null)"""
  note: String        # was String!
  memo: String        # Optional[str] output — was String! by convention
}

⚠️ Behavioral change: every nullable output field on the entity-first SDL loses its ! suffix. Introspection output was already nullable for all of these (entity introspection never emitted NON_NULL), so this aligns the two renderers rather than changing what clients execute against.

3. Cleanup (from the issue) —

  • New annotation_is_nullable(annotation) in graphql/type_mapping.py is the single predicate for NON_NULL decisions; all former not _is_optional(...) force_non_null computations in compose_schema.py now use it so SDL and type-ref renders cannot drift again.
  • _optional_core dedups the Optional-unwrap step previously copied in literal_info and _literal_constraint.
  • literal_allowed_values removed — no library callers since fix: preserve nullability in Literal descriptions #318 (tests updated; its coverage is subsumed by the fix: preserve nullability in Literal descriptions #318 parametrized description tests).

Verification

🤖 Generated with Claude Code

…nnotations

- compose: _render_struct_type_sdl decided the ! suffix via _is_optional
  alone and missed Literal[..., None], which its sibling _build_type_ref
  does handle — SDL and introspection disagreed within the same module.
- entity: map_to_sdl no longer forces NON_NULL on output for nullable
  annotations (Optional[T] / T | None / Literal[..., None]); output SDL
  now matches map_to_introspection and the Python annotation (#320
  option A; behavioral change — such fields lose the ! suffix).
- extract annotation_is_nullable as the single predicate for NON_NULL
  decisions; dedup the Optional-unwrap step shared by literal_info and
  _literal_constraint (_optional_core); drop literal_allowed_values
  (no library callers since #318).

Fixes #320

Co-Authored-By: Claude Code <noreply@anthropic.com>
@allmonday
allmonday merged commit 50220ae into master Oct 6, 2026
6 checks passed
allmonday added a commit that referenced this pull request Oct 6, 2026
Literal description accuracy and nullability consistency release:
Allowed values descriptions render GraphQL literals and advertise
null (#318), and SDL nullability aligns with introspection (#320,
#321 — nullable output fields lose the ! suffix).

Co-Authored-By: Claude Code <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

Development

Successfully merging this pull request may close these issues.

Output-side nullable Literal fields render NON_NULL in SDL while introspection and descriptions say nullable

1 participant