# config.json Compatibility

`Case.configure_forcings()` writes `<inputdir>/extract_forcings/config.json`.
`process_forcings()`, `crocodash process` and `crocodash dump` read it back,
often with a newer CrocoDash than the one that wrote it. This page explains
how to change a configurator without breaking the cases that already exist.

This is about cases this CrocoDash can read at all. Reading a case starts with
its `_crocodash_state.json`, and a case whose state file is older than this
CrocoDash supports is refused before its config.json is read: it has to be
recreated. Tolerant reading keeps the cases it accepts working as configurators
gain inputs and outputs.

## Reading and writing

Always go through `read_config(path)` and `write_config(path, config)` in
`CrocoDash/forcing/base.py`. Never call `json.load` on config.json directly.
`read_config` does two things:

- it upgrades the file through `CONFIG_MIGRATIONS`;
- it checks its `schema_version` against `CONFIG_SCHEMA_VERSION`. MAJOR.MINOR
  must match, and a file written before versioning counts as `1.0.0`.

A test fails if a module builds a config.json path on its own.

## Tolerant reading

`BaseConfigurator.deserialize` reads each configurator's entry tolerantly:

- **Input missing from the entry:** it takes the constructor's default. If the
  input has no default, `ReconfigureRequired` is raised.
- **Input missing from the entry whose default is `None`:** `None` is often a
  placeholder that `configure_forcings()` fills in from the case, the run dates
  or the product (a calendar, a start date). Unless the input is declared
  `optional=True`, reading it raises `ReconfigureRequired`.
- **Output missing from the entry:** reading it raises `ReconfigureRequired`.
- **Key the class no longer declares:** it is ignored.
- **Entry the constructor rejects** (`ValueError` or `TypeError` from
  `validate_args`), **or an entry for a configurator that no longer exists:**
  `ReconfigureRequired` is raised.

The entry still loads in the two "reading it raises" cases, so the steps that
don't need the missing value still run, and the one that does asks for
`configure_forcings()` again instead of failing on a `None`.

So you can usually add an input or an output without touching the version:

- give a new input a default;
- declare it `optional=True` only if the configurator really works with it
  left `None` (e.g. CICE's `cice_product_name`: `None` means no restoring).

Only `validate_args` should reject bad inputs, and it runs again whenever
config.json is read back, possibly on another machine. So it doesn't check that
input files exist. `BaseConfigurator.configure()` warns about every
`InputFileParam` whose file is missing, and processing raises when a step needs
one.

## When to bump the version

Bump the MINOR of `CONFIG_SCHEMA_VERSION` when tolerant reading would
*misread* an old file. Renaming a param is the usual case: the old value would
be ignored, and the new param would silently take its default. In the same PR:

1. Add a migration to `CONFIG_MIGRATIONS`, keyed by the version it upgrades
   from (`"1.0"`), that rewrites the old layout and sets `schema_version` to
   the new version. `read_config` raises if a migration leaves the version
   unchanged.
2. Add a config.json from a real case to `tests/forcing/golden_configs/` if
   none of the existing ones covers the change.

The golden configs in `tests/forcing/golden_configs/` are config.json files
real cases wrote, with personal paths replaced. Every one must keep loading.
