Application construction

DataSchemer validates input DataCanon Instances (DCIs) and applies schema defaults. Application constructors receive instances of conventional types, such as int, list, and NumPy arrays. A constructor can then perform the transformations its application requires.

Keep validation and application transformations separate

import numpy as np

from data_canon import as_dc_instance
from data_schemer.schema_projector import SchemaProjector


class Calculation:
    def __init__(self, matrix, iterations, tolerance=1e-8):
        self.matrix = np.asarray(matrix, dtype=np.float64)
        self.iterations = iterations
        self.tolerance = tolerance


schema = {
    "variables": {
        "coefficients": {"code_alias": "matrix"},
        "iterations": {"type": "Integer"},
        "tolerance": {"type": "Float", "optional": True},
    },
}

input_dci = as_dc_instance({
    "coefficients": [[1, 0], [0, 1]],
    "iterations": 20,
})
projection = SchemaProjector(schema).get_projection(input_dci)
calculation = projection.get_instance(Calculation)

assert calculation.matrix.shape == (2, 2)
assert calculation.matrix.dtype == np.dtype("float64")
assert calculation.tolerance == 1e-8
assert projection.data_dci == input_dci

The schema requires an Integer DCI for iterations. The untyped coefficients field can accept a nested List DCI or an Array DCI. Conventional conversion produces the corresponding objects; Calculation chooses its own array dtype. That transformation does not alter the processed DCI. The code_alias maps the external field name coefficients to constructor argument matrix.

Schema defaults and constructor defaults

A schema default becomes part of the processed DCI and is passed to a matching constructor argument. An absent optional field without a schema default remains absent, allowing the constructor’s own default to apply. Explicitly supplied values, including Null, take precedence over defaults when allowed by the schema.

In the example, tolerance remains absent from projection.data_dci, while the constructed object uses Calculation’s default of 1e-8. Adding "default": 1e-6 to the field definition would put that value in the processed DCI and pass it to the constructor instead.

A default does not infer a type constraint:

variables = {"value": {"optional": True, "default": 1}}

The default is an Integer DCI, but an explicitly supplied value may have any DataCanon Type (DCT). Add "type": "Integer" to constrain both supplied values and the default. DataSchemer (DS) never uses constructor annotations or constructor defaults to infer DCT constraints. A constructor default cannot satisfy a required schema field; schema validation happens before application construction.

Constructor argument selection

get_instance() accepts a class or callable factory with an inspectable signature. It selects matching local fields after applying code_alias, and passes arguments by keyword. Keyword-only parameters are supported. An omitted parameter retains its callable default.

Other local fields are ignored unless the callable accepts **kwargs, in which case additional fields are passed using their aliases. An unused external reference is not materialized merely to construct an object. Similarly, a registered Record conversion handler runs only if its value is selected for conversion.

Signature binding happens before conventional conversion. Missing constructor arguments raise DSUserError without first retrieving external payloads or calling Record conversion handlers. A required positional-only argument needs a wrapper that accepts keyword arguments:

obj = projection.get_instance(lambda matrix, iterations: factory(matrix, iterations))

A callable without an inspectable signature also needs an inspectable wrapper. Exceptions raised by the application constructor or by conventional conversion propagate to the caller; DS does not disguise them as schema validation errors.

Child projections

Root construction uses local fields only, even if the root constructor accepts **kwargs. Child data is not automatically passed to a root constructor, nor does DS choose classes for child schemas.

Construct a child object explicitly from its projection:

solver_projection = projection.child_projections["solver"]
solver = solver_projection.get_instance(Solver)

The application decides how to assemble independently constructed objects. data_tree_dci and data_tree remain available when the application wants the complete nested data instead. A child projection exists only when its Mapping was supplied; an empty supplied Mapping receives defaults and validation.

SchemaCommandLine

Passing target_class=Calculation to SchemaCommandLine uses the same construction contract and makes the result available through target_obj. Standalone projection and SchemaCommandLine (SCL) share one implementation of argument selection, alias handling, deferred conversion, and signature checking.