Skip to content

A bare noul question and state=None pass client-side validation but are refused by the API #17

Description

@kittimzhe

Summary

Two request shapes pass all client-side validation and only fail on the server:

  1. A noul question with neither instructions nor criteria. The API answers 400 Noul question must have criteria or instructions — the rule was live-verified against jev-latest in typesafe-ai/typesafe-sdk-js#6, which also shows that empty criteria ({}) and criteria whose sides are both null count as no criteria.
  2. state=None. The API answers 422 Field required for body.state — null is treated as missing (same live verification).

Both sync and async clients are affected (they share prepare_system_one).

Repro (offline, no API key)

from typesafe_sdk import Noul
from typesafe_sdk._core.config import Config
from typesafe_sdk._core.endpoints import prepare_system_one
from typesafe_sdk._core.response_types import SystemOneResponse

cfg = Config(api_key="offline", base_url="https://example.invalid",
             default_model="jev-latest", timeout=10.0, default_headers={})

prepare_system_one(cfg, "s", {"q": Noul()}, None, None, None, None, SystemOneResponse)
# passes; wire body: {"state":"s","model":"jev-latest","questions":{"q":{"type":"noul"}}}

prepare_system_one(cfg, "s", {"q": Noul(criteria={})}, None, None, None, None, SystemOneResponse)
# passes; "criteria":{} — counts as no criteria per the JS issue's live table

prepare_system_one(cfg, "s", {"q": Noul(criteria={"true": None, "false": None})}, None, None, None, None, SystemOneResponse)
# passes; both-null sides count as no criteria

prepare_system_one(cfg, None, {"q": Noul(instructions="x")}, None, None, None, None, SystemOneResponse)
# passes; wire body starts {"state":null,...

I verified the wire bytes locally (typesafe-sdk 0.7.2); the 400/422 refusals are cross-referenced from the JS issue's live verification of the same endpoint.

Where the gap lives

  • normalize_questions() (_core/questions.py) already fail-fasts on a Score question with empty criteria — there is no matching check that a Noul has instructions or criteria (neither the object path nor the raw-dict path).
  • prepare_system_one() (_core/endpoints.py) serializes state as-is. The wire SystemOneRequest.state is typed str | dict | list — null isn't even in the wire type — but nothing enforces that on the request path, so a None that is legal for the Python signature goes out as null.

This is the same family as #12 (Choice criteria): the system_one docstring already promises a local TypeSafeError for empty questions and empty score criteria — these are two more holes in that net.

Suggested directions

  1. Extend normalize_questions() with the noul check mirroring the score one (instructions or criteria, where {}/all-null counts as none).
  2. Reject state=None explicitly, or validate the outgoing body against the wire SystemOneRequest before send.
  3. If Choice questions allow empty criteria, but Score questions don't #12 lands the Choice check, these could ride the same pattern.

Happy to paste a patch for either direction here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions