> This page is for version 0.7.0.
> For other versions, use one of these documentation indexes:
> - Main (default): https://docs.nvidia.com/nemo/gym/main/llms.txt
> - 0.7.0: https://docs.nvidia.com/nemo/gym/v0.7.0/llms.txt
> - 0.6.0: https://docs.nvidia.com/nemo/gym/v0.6.0/llms.txt
> - 0.5.1: https://docs.nvidia.com/nemo/gym/v0.5.1/llms.txt
> - 0.5.0: https://docs.nvidia.com/nemo/gym/v0.5.0/llms.txt
> - 0.4.0: https://docs.nvidia.com/nemo/gym/v0.4.0/llms.txt
> - 0.3.0: https://docs.nvidia.com/nemo/gym/v0.3.0/llms.txt
> - 0.2.1: https://docs.nvidia.com/nemo/gym/v0.2.1/llms.txt

> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.nvidia.com/nemo/gym/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.nvidia.com/nemo/gym/_mcp/server.

# nemo_gym.data.tasks.harbor.task_data

The dataset's `task_data.py`: the `TaskData` model every row's task fields are validated against.

A Gym-authored dataset may ship `task_data.py` beside `dataset.toml`, a module exporting one
Pydantic model named `TaskData` that declares the task-owned fields of its rows. A task's fields
come from `task_data.json` in its folder (a parameterized task's rendered row; a hand-written task
may carry one too) and travel in the row's `task_input.task_data` beside the reserved `_ng_*`
provenance keys. The same check runs at the three points a row crosses a boundary:

* at materialize, `prepare_target` loads the module once per dataset and refuses the whole
  dataset, naming the offending task, when a task's fields do not satisfy the model (the folders'
  `task_data.json` files are checked before any image is recorded or built, the loaded tasks again
  before a row is written);
* at seed, the `harbor` environment server validates the row it is handed and refuses the
  episode (422, not retried) when it does not satisfy the model or differs from the task folder's;
* at collate, `gym dataset collate` validates every row of the taskset the same way.

Unknown keys fail everywhere: `TaskData` is the complete description of the fields, whatever its
`extra` setting, so a typo in a row cannot pass as an optional field, and neither can a key no
field reads (a field's name where only its alias is accepted, a second spelling of a field already
given): the model is validated with extras forbidden, so pydantic's own accepted input names decide.
Values are validated strictly, as JSON: a value of another JSON type (`"3"` for an `int`, `"no"`
for a `bool`) is refused, not coerced, because the row and the verifier read the file's values as
written, never the model's. The reserved keys are never part of `TaskData` and never admitted as
task fields. A dataset without `task_data.py` is not checked beyond the reserved keys (every hub
dataset).

The module is imported, not parsed: `TaskData` is a plain Pydantic class and may use whatever the
standard library and Pydantic offer. It is loaded under a module name derived from its path, so two
datasets with the same folder name keep separate modules.

## Module Contents

### Classes

| Name                                                                               | Description                                                                       |
| ---------------------------------------------------------------------------------- | --------------------------------------------------------------------------------- |
| [`DatasetRowValidator`](#nemo_gym-data-tasks-harbor-task_data-DatasetRowValidator) | The collator's validator for a Harbor taskset whose dataset ships `task_data.py`. |
| [`TaskDataError`](#nemo_gym-data-tasks-harbor-task_data-TaskDataError)             | `task_data.py` does not load, or a task's fields do not satisfy its `TaskData`.   |
| [`TaskDataSchema`](#nemo_gym-data-tasks-harbor-task_data-TaskDataSchema)           | A loaded `task_data.py`: where it came from and the model it exports.             |

### Functions

| Name                                                                                         | Description                                                                                                    |
| -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| [`_format_validation_error`](#nemo_gym-data-tasks-harbor-task_data-_format_validation_error) | -                                                                                                              |
| [`check_row_matches_folder`](#nemo_gym-data-tasks-harbor-task_data-check_row_matches_folder) | Refuse a row whose task fields are not the task folder's: the row was edited after materialization.            |
| [`check_task_folders`](#nemo_gym-data-tasks-harbor-task_data-check_task_folders)             | Validate each folder's `task_data.json` against `schema` before the tasks load; the first violation refuses    |
| [`check_tasks`](#nemo_gym-data-tasks-harbor-task_data-check_tasks)                           | Validate every loaded task's fields against `schema`; the first violation refuses the dataset.                 |
| [`is_reserved_key`](#nemo_gym-data-tasks-harbor-task_data-is_reserved_key)                   | -                                                                                                              |
| [`load_task_data_schema`](#nemo_gym-data-tasks-harbor-task_data-load_task_data_schema)       | Import `&lt;dataset_root&gt;/task_data.py`; `None` when the dataset ships none.                                |
| [`read_task_fields`](#nemo_gym-data-tasks-harbor-task_data-read_task_fields)                 | The task's own field values from `&lt;task_folder&gt;/task_data.json`; `&#123;&#125;` when the file is absent. |
| [`task_fields`](#nemo_gym-data-tasks-harbor-task_data-task_fields)                           | The task-owned part of a row's `task_data`: everything but the reserved `_ng_*` keys.                          |

### Data

[`RESERVED_PREFIX`](#nemo_gym-data-tasks-harbor-task_data-RESERVED_PREFIX)

[`TASK_DATA_EXPORT`](#nemo_gym-data-tasks-harbor-task_data-TASK_DATA_EXPORT)

[`TASK_DATA_FILE`](#nemo_gym-data-tasks-harbor-task_data-TASK_DATA_FILE)

[`TASK_DATA_MODULE`](#nemo_gym-data-tasks-harbor-task_data-TASK_DATA_MODULE)

### API

```python
class nemo_gym.data.tasks.harbor.task_data.DatasetRowValidator(
    server_name: str,
    adapter: pydantic.TypeAdapter,
    dataset_fpath: str,
    schema: nemo_gym.data.tasks.harbor.task_data.TaskDataSchema | None = None
)
```

**Bases:** [TaskDataValidator](/nemo/gym/nemo-gym/nemo_gym/data/task_data#nemo_gym-data-task_data-TaskDataValidator)

The collator's validator for a Harbor taskset whose dataset ships `task_data.py`.

A materialized row's `task_input.task_data` is two things in one mapping: the framework's reserved
`_ng_*` keys, which the environment server's schema describes, and the task-owned fields beside them,
which belong to the dataset (`task_data.json`) and to its `TaskData` when it ships one. The two are
validated separately into one report, as at prepare and seed: the server's schema sees the reserved keys
alone, and the dataset's model, if any, the whole `task_data` (`task_fields` drops the reserved
keys for it). Fed the whole mapping, the server's validator would report every task field of an untyped
dataset as unknown and splice a task's own `verifier_metadata` mapping apart as the legacy row format
(`nemo_gym.data.task_data.normalize_task_fields`). A dataset without `task_data.py` is checked for the
reserved keys only.

```python
nemo_gym.data.tasks.harbor.task_data.DatasetRowValidator.validate_row(
    row_index: int,
    row: dict[str, typing.Any]
) -> None
```

```python
class nemo_gym.data.tasks.harbor.task_data.TaskDataError()
```

**Bases:** [HarborTaskError](/nemo/gym/nemo-gym/nemo_gym/data/tasks/harbor/errors#nemo_gym-data-tasks-harbor-errors-HarborTaskError)

`task_data.py` does not load, or a task's fields do not satisfy its `TaskData`.

```python
class nemo_gym.data.tasks.harbor.task_data.TaskDataSchema(
    path: pathlib.Path,
    model: type[pydantic.BaseModel]
)
```

Dataclass

A loaded `task_data.py`: where it came from and the model it exports.

**`model`** `type[BaseModel]`

---

**`path`** `Path`

---

**`strict_model`** `type[BaseModel]`

`TaskData` with `extra="forbid"`: the model that validates rows.

Pydantic alone knows which input names a field reads (its name, its alias or `validation_alias`, the
first of several `AliasChoices` present, subject to `validate_by_name`/`validate_by_alias`). With
extras forbidden it refuses every key no field consumed, so a key that would otherwise be dropped on
the floor while the field takes its default or another spelling's value is an error, not a silently
unvalidated value: `{"difficulty": "x"}` against `difficulty: int = Field(default=1, alias="level")`,
or `expected_answer` given beside the `answer` the field read first. The author's model is left as
it is; this subclass only forbids extras on top of it.

---

```python
nemo_gym.data.tasks.harbor.task_data.TaskDataSchema.validate(
    task_data: typing.Mapping[str, typing.Any],
    task_id: str
) -> pydantic.BaseModel
```

The row's task fields as an instance of `TaskData`; raises `TaskDataError` naming the task.

A top-level key no field of the model reads (`strict_model`) is refused as undeclared before any other
error is reported. An extra key inside a nested model the author marked `extra="forbid"` is not a key of
`TaskData`: it stays a validation error at its location (`meta.bogus: Extra inputs are not permitted`),
reported beside the row's other value errors. Whatever else the model raises is that error: a pydantic `ValidationError` with its
fields spelled out, or anything else an author's validator lets escape (a `KeyError` from a lookup, a
`TypeError` on a value of the wrong type), named with its type. Every caller (prepare, the seed, collate)
handles one exception class, so a validator defect refuses the dataset, fails the episode as terminal or is
reported against the row instead of crashing the command or being retried as a provider failure.

The values are validated strictly, in JSON terms: the fields are what a JSON file holds, so the model reads
them as JSON (`model_validate_json` with `strict=True`), and a value of another JSON type is refused,
never coerced. The model returned here is discarded by every caller: the row carries the file's values as
written, and so does the `task_data.json` the in-sandbox verifier reads (`task.task_data`, the same
fields). Lax validation would accept `{"flag": "no"}` against `flag: bool` (reading `False`) and
`{"count": "3"}` against `count: int` while the verifier reads the strings, `"no"` truthy among them.
JSON's own spellings of a type still pass, as they must for a file to hold them at all: an ISO string for a
`datetime`, an enum's value, an integer where a `float` is declared. A validator that rewrites a value
changes the model, not the row: write the value the verifier should read in the file, and let a validator
refuse rather than normalize.

The key check stays a pass over the fields as Python values, lax, because only there does pydantic's
`extra="forbid"` count the one input name a field consumed: fed JSON, it treats every name a field could
read as known, so a field's name beside its alias, or a second `AliasChoices` spelling beside the first,
would pass unvalidated. That pass is forced lax (`strict=False`) because `strict_model` inherits the
author's `model_config`, and an author's `strict=True` would otherwise refuse JSON's spellings there (an
ISO string for a `datetime`) that the JSON pass, which alone decides the value types, accepts. Both passes
are pydantic's own; the first also runs the author's validators.

```python
nemo_gym.data.tasks.harbor.task_data._format_validation_error(
    error: pydantic.ValidationError
) -> str
```

```python
nemo_gym.data.tasks.harbor.task_data.check_row_matches_folder(
    row_task_data: typing.Mapping[str, typing.Any],
    task: typing.Any
) -> None
```

Refuse a row whose task fields are not the task folder's: the row was edited after materialization.

The digest the row carries pins the folder; this pins the row to it. Raises `TaskDataError`.

```python
nemo_gym.data.tasks.harbor.task_data.check_task_folders(
    schema: nemo_gym.data.tasks.harbor.task_data.TaskDataSchema | None,
    task_folders: typing.Any
) -> None
```

Validate each folder's `task_data.json` against `schema` before the tasks load; the first violation refuses
the dataset.

`task_folders` are paths. The fields need no loaded task, so prepare runs this right after the module imports,
before any base image is resolved or any Dockerfile built and pushed for a dataset that is refused as a whole
anyway; `check_tasks` over the loaded tasks stays the final guard. A file that does not read (not a JSON
object, a reserved key) is left to the loader, which skips that task with the file's error. Without a schema
nothing is checked.

```python
nemo_gym.data.tasks.harbor.task_data.check_tasks(
    schema: nemo_gym.data.tasks.harbor.task_data.TaskDataSchema | None,
    tasks: typing.Any
) -> None
```

Validate every loaded task's fields against `schema`; the first violation refuses the dataset.

`tasks` are `HarborTask` instances, checked in order. Without a
schema nothing is checked: a dataset that ships no `task_data.py` behaves as before.

```python
nemo_gym.data.tasks.harbor.task_data.is_reserved_key(
    key: str
) -> bool
```

```python
nemo_gym.data.tasks.harbor.task_data.load_task_data_schema(
    dataset_root: pathlib.Path
) -> nemo_gym.data.tasks.harbor.task_data.TaskDataSchema | None
```

Import `<dataset_root>/task_data.py`; `None` when the dataset ships none.

Raises `TaskDataError` when the module does not import, exports no `TaskData`, or
exports one that is not a Pydantic model class.

```python
nemo_gym.data.tasks.harbor.task_data.read_task_fields(
    task_folder: pathlib.Path
) -> dict[str, typing.Any]
```

The task's own field values from `<task_folder>/task_data.json`; `{}` when the file is absent.

The file must hold a JSON object whose keys are not reserved. Raises `TaskDataError` otherwise.

```python
nemo_gym.data.tasks.harbor.task_data.task_fields(
    task_data: typing.Mapping[str, typing.Any]
) -> dict[str, typing.Any]
```

The task-owned part of a row's `task_data`: everything but the reserved `_ng_*` keys.

```python
nemo_gym.data.tasks.harbor.task_data.RESERVED_PREFIX = '_ng_'
```

```python
nemo_gym.data.tasks.harbor.task_data.TASK_DATA_EXPORT = 'TaskData'
```

```python
nemo_gym.data.tasks.harbor.task_data.TASK_DATA_FILE = 'task_data.json'
```

```python
nemo_gym.data.tasks.harbor.task_data.TASK_DATA_MODULE = 'task_data.py'
```