Module adcp.validation.oneof_hints

Heuristic hint strings for oneOf near-miss validation failures.

When a payload fails a discriminated-union (oneOf) shape because the caller used the wrong key as the discriminator (the v3 ref seller pricing_options regression: {"type": "cpm", ...} instead of {"pricing_model": "cpm", ...}), the standard jsonschema diagnostic is "<value> is not valid under any of the given schemas" — accurate but unactionable for an LLM client.

This module computes an additive hint string that names the closest matching variant and the wrong / expected discriminator keys:

Looks like you may have meant the 'cpm' variant. Use 'pricing_model'
instead of 'type' as the discriminator.

The hint is best-effort: if no clear winner exists across variants, no hint is emitted (silent is better than misleading).

Functions

def compute_oneof_hint(schema: dict[str, Any],
schema_path_segments: list[Any],
instance_path_segments: list[Any],
payload: Any) ‑> str | None
Expand source code
def compute_oneof_hint(
    schema: dict[str, Any],
    schema_path_segments: list[Any],
    instance_path_segments: list[Any],
    payload: Any,
) -> str | None:
    """Compute a near-miss hint for an ``oneOf`` failure.

    Args:
        schema: The compiled validator's root schema (refs already inlined).
        schema_path_segments: ``absolute_schema_path`` from the validation
            error — points at the ``oneOf`` keyword.
        instance_path_segments: ``absolute_path`` from the validation
            error — points at the offending value in the payload.
        payload: The full request/response payload that failed validation.

    Returns the hint string, or ``None`` if the heuristic can't pick a
    clear winner (no detectable discriminator, no clear best variant,
    or no obvious wrong-discriminator key).
    """
    if not schema_path_segments or schema_path_segments[-1] != "oneOf":
        return None

    parent = _navigate(schema, list(schema_path_segments[:-1]))
    if not isinstance(parent, dict):
        return None
    variants_raw = parent.get("oneOf")
    if not isinstance(variants_raw, list) or len(variants_raw) < 2:
        return None
    variants: list[dict[str, Any]] = [v for v in variants_raw if isinstance(v, dict)]
    if len(variants) < 2:
        return None

    value = _navigate_input(payload, list(instance_path_segments))
    if not isinstance(value, dict):
        return None

    discriminator = _detect_discriminator(variants)
    if discriminator is None:
        return None

    # Skip the hint when the caller already used the right discriminator
    # — they merely picked a value that doesn't match any variant. The
    # default "value not in allowed enum" message is more accurate there.
    if discriminator in value:
        return None

    scored = [(_score_variant(v, value, discriminator), idx, v) for idx, v in enumerate(variants)]
    # Sort by const_match (strongest), then required_present, then total_present.
    # seen_key (index 3 of the score tuple) is metadata, not a ranking signal.
    scored.sort(key=lambda s: (-s[0][0], -s[0][1], -s[0][2], s[1]))

    best_score, _, best_variant = scored[0]
    if len(scored) > 1:
        runner_up = scored[1][0]
        # Compare the ranking signals only — ignore seen_key.
        if best_score[:3] == runner_up[:3]:
            # No clear winner; silent rather than misleading.
            return None

    if best_score[:3] == (0, 0, 0):
        return None

    expected_const = _variant_const_value(best_variant, discriminator)
    if expected_const is None:
        return None

    # Prefer the seen_key recorded during scoring (exact key/value match
    # against the winning variant's const). Fall back to "extraneous
    # top-level key" only when no const-carrying key was found.
    seen_key = best_score[3] or _fallback_seen_key(value, discriminator, variants)
    if seen_key is None:
        return None

    return (
        f"Looks like you may have meant the {expected_const!r} variant. "
        f"Use {discriminator!r} instead of {seen_key!r} as the discriminator."
    )

Compute a near-miss hint for an oneOf failure.

Args

schema
The compiled validator's root schema (refs already inlined).
schema_path_segments
absolute_schema_path from the validation error — points at the oneOf keyword.
instance_path_segments
absolute_path from the validation error — points at the offending value in the payload.
payload
The full request/response payload that failed validation.

Returns the hint string, or None if the heuristic can't pick a clear winner (no detectable discriminator, no clear best variant, or no obvious wrong-discriminator key).