Diagnosing Errors — A Tour
Mix.install([
{:wpl_validator, path: Path.join(__DIR__, "..")},
{:jason, "~> 1.4"},
{:kino, "~> 0.13"}
])
Goal
Walk through every error code WPL Validator can emit, see what triggers it,
and how to fix it. Each section loads a real fixture from
priv/conformance/invalid/, validates it, then shows a corrected version.
defmodule Tour do
@conf Path.join([__DIR__, "..", "priv", "conformance", "invalid"])
def load(name) do
@conf |> Path.join(name) |> File.read!() |> Jason.decode!()
end
def validate(plan, opts \\ []) do
WPL.Validator.validate(plan, opts)
end
def render(plan, opts \\ []) do
result = validate(plan, opts)
rows =
Enum.map(result.errors, fn e ->
%{severity: e.severity, code: e.code, path: e.path, meta: inspect(e.meta)}
end)
Kino.Layout.grid(
[
Kino.Markdown.new(if(result.valid?, do: "**valid? true**", else: "**valid? false**")),
Kino.DataTable.new(rows)
],
boxed: true
)
end
end
:schema_violation — Pass 1 stops everything
When the JSON Schema rejects the document, Pass 2 (semantic checks) is skipped entirely. Fix schema violations first, then re-validate.
Tour.load("schema-violation-bad-enum.json") |> Tour.render()
Fix: change "type": "intergalactic_disco_party" to one of the allowed
plan types (workout, nutrition, meditation, recovery, hybrid).
:duplicate_id — sibling collision
Within a scope (plan/phase/week/day), every id must be unique. The error
points at the second occurrence and includes a :first_occurrence JSON
Pointer.
Tour.load("duplicate-day-id.json") |> Tour.render()
Fix: rename the offender. The walker tracks scopes hierarchically:
-
phase ids are scoped to
plan -
week ids are scoped to
phase: -
day ids are scoped to
week: -
block & activity ids are scoped to
day:(so two activities can share an id in different days).
:unresolved_ref — catalog-only check
exercise_ref values must exist in the catalog you pass via the catalog:
option. If you don’t pass a catalog, this check is skipped entirely.
plan = Tour.load("unresolved-exercise-ref.json")
# Without catalog — no error
Tour.render(plan)
# With catalog — surfaces the unknown ref
Tour.render(plan, catalog: %{exercises: MapSet.new(["push_up", "squat"])})
Fix: add the missing exercise to your catalog, or change the
exercise_ref to one that exists.
:invalid_prescription — sets_reps without sets or reps
A sets_reps prescription needs at least one of sets or reps populated.
Tour.load("invalid-prescription-sets-reps.json") |> Tour.render()
Fix: add "sets": 3 or a "reps": {"target": 10} block.
:invalid_personalization_rule — unknown action type
Personalization actions must use a known type. The full list:
modify_intensity, add_warmup_time, increase_rest, reduce_sets, reduce_reps, replace_exercise, exclude_exercise, modify_exercise, use_schedule, add_activity.
Tour.load("invalid-personalization-action.json") |> Tour.render()
Fix: replace "set_world_on_fire" with a known action type.
:invalid_personalization_rule — bad action scope
Action scope must be one of: activity, block, day, week, phase, plan.
Tour.load("invalid-personalization-bad-scope.json") |> Tour.render()
Fix: change "scope": "galaxy" to one of the allowed scopes.
:invalid_points_rule — negative points
Points must be a non-negative integer.
Tour.load("invalid-points-rule.json") |> Tour.render()
Fix: change "points": -10 to a non-negative number.
:empty_phases_for_type — workout without phases
Plans of type workout or hybrid need at least one phase.
Tour.load("empty-phases-workout.json") |> Tour.render()
Fix: add a phase, or change the plan type to one that doesn’t require
phases (nutrition, meditation, recovery).
:phase_duration_mismatch — warning, not error
If a phase declares duration: 3 weeks but the weeks array has 2 items,
you get a warning. Warnings don’t invalidate the plan but indicate likely
authoring mistakes.
Tour.load("phase-duration-mismatch.json") |> Tour.render()
Fix: either bring the array length in line with duration, or remove
the duration field entirely (it’s optional).
:cyclic_subplan — sub-plan reference cycle (preview, schema v1.5.0+)
> Preview feature. SubPlanActivity is reserved schema surface for
> a future plan-marketplace / library-composition use case. Most plans
> should not use it — express composition by inlining content. The
> validator rule below exists so the surface is not foot-gun-prone if
> you do choose to use it.
A SubPlanActivity whose sub_plan_ref points back to the containing
plan’s own id creates a self-cycle. Compiling such a plan would loop
forever; the validator catches it before consumers do.
Tour.load("cyclic-subplan-self.json") |> Tour.render()
Fix: change the sub_plan_ref to a different plan id, or remove the
sub-plan activity entirely. Cross-plan cycles (A → B → A) require a
future sub_plans resolution map at validate time and are not yet
detected by single-plan validation.
Summary
-
Pass 1 errors (
:schema_violation) shadow Pass 2 — fix them first. - Pass 2 errors check things JSON Schema can’t express: cross-document uniqueness, ref resolution, semantic invariants.
-
Warnings (e.g.,
:phase_duration_mismatch) surface authoring mistakes without invalidating the plan.
For the canonical list and stability guarantees, see
priv/conformance/error-codes.md.