Coercing Schemas
Coercion in Data Joinery checks or converts an input DataFrame against a specified schema. There are several modes available to control how strict/relaxed the coercion process should be for your transformations.
Use Schema(Model).coerce_dataframe() to take an input DataFrame and return either a new DataFrame that
matches the model's schema or raise a validation error if coercion fails based on the mode's strictness.
Let's take the following schema and walk through how each mode works:
from dataclasses import dataclass
from data_joinery import Schema
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
Coercion Modes
Depending on your workflow and needs, you can choose from several coercion modes.
Strict (Default)
In strict mode, the input DataFrame must have exactly the same named fields and types as the
specified model. Any extra columns, missing columns or type mismatches will result in an error.
Field order and nullability are ignored, and the input field order is preserved. This DataFrame
has an extra column so will fail the strict validation check:
import traceback
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema, SchemaCoercionError
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class ModelWithExtraColumn:
field1: str
field2: str
sub_schema: SubModel
extra_field: str
input_schema = Schema(ModelWithExtraColumn)
input_df = input_schema.create_dataframe(
[
ModelWithExtraColumn(
field1="value1",
field2="value2",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
extra_field="extra_value",
)
],
pl.LazyFrame,
)
try:
schema.coerce_dataframe(input_df, mode="strict")
except SchemaCoercionError:
print(traceback.format_exc(limit=1))
Outputs:
Traceback (most recent call last):
File "/home/runner/work/data-joinery/data-joinery/docs_src/learn/coercion/dataclass_coercion_strict_fail_extra_column_polars.py", line 49, in <module>
schema.coerce_dataframe(input_df, mode="strict")
data_joinery.schemas.coercion_error.SchemaCoercionError: Schema 'Model': Cannot coerce DataFrame (mode='strict'):
- Additional field 'extra_field': got String. Remove it or add it to the model.
import traceback
from dataclasses import dataclass
from pyspark.sql import DataFrame, SparkSession
from data_joinery import Schema, SchemaCoercionError
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class ModelWithExtraColumn:
field1: str
field2: str
sub_schema: SubModel
extra_field: str
input_schema = Schema(ModelWithExtraColumn)
spark = SparkSession.builder.getOrCreate()
input_df = input_schema.create_dataframe(
[
ModelWithExtraColumn(
field1="value1",
field2="value2",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
extra_field="extra_value",
)
],
DataFrame,
session=spark,
)
try:
schema.coerce_dataframe(input_df, mode="strict")
except SchemaCoercionError:
print(traceback.format_exc(limit=1))
Outputs:
Traceback (most recent call last):
File "/home/runner/work/data-joinery/data-joinery/docs_src/learn/coercion/dataclass_coercion_strict_fail_extra_column_spark.py", line 52, in <module>
schema.coerce_dataframe(input_df, mode="strict")
data_joinery.schemas.coercion_error.SchemaCoercionError: Schema 'Model': Cannot coerce DataFrame (mode='strict'):
- Additional field 'extra_field': got StringType(). Remove it or add it to the model.
However if the Dataframe is equivalent it will pass and return the same DataFrame:
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class SubModel2:
field2: str
field1: str
sub_schema: SubModel
input_schema = Schema(SubModel2)
input_df = input_schema.create_dataframe(
[
SubModel2(
field1="value1",
field2="value2",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
)
],
pl.LazyFrame,
)
coerced_df = schema.coerce_dataframe(input_df, mode="strict")
print(coerced_df.collect())
Outputs:
from dataclasses import dataclass
from pyspark.sql import DataFrame, SparkSession
from data_joinery import Schema
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class SubModel2:
field2: str
field1: str
sub_schema: SubModel
input_schema = Schema(SubModel2)
spark = SparkSession.builder.getOrCreate()
input_df = input_schema.create_dataframe(
[
SubModel2(
field1="value1",
field2="value2",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
)
],
DataFrame,
session=spark,
)
coerced_df = schema.coerce_dataframe(input_df, mode="strict")
coerced_df.show()
Outputs:
Strict does not care about ordering of the fields.
Project Top Level
project_top_level projects only the model's top-level columns. If there is a difference in struct fields it will fail:
import traceback
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema, SchemaCoercionError
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class SubModelWithExtraField:
subfield1: str
subfield2: str
extra_field: str
@dataclass
class ModelWithExtraStructField:
field1: str
field2: str
sub_schema: SubModelWithExtraField
input_schema = Schema(ModelWithExtraStructField)
input_df = input_schema.create_dataframe(
[
ModelWithExtraStructField(
field1="value1",
field2="value2",
sub_schema=SubModelWithExtraField(
subfield1="subvalue1", subfield2="subvalue2", extra_field="extra_value"
),
)
],
pl.LazyFrame,
)
try:
schema.coerce_dataframe(input_df, mode="project_top_level")
except SchemaCoercionError:
print(traceback.format_exc(limit=1))
Outputs:
Traceback (most recent call last):
File "/home/runner/work/data-joinery/data-joinery/docs_src/learn/coercion/dataclass_coercion_project_top_level_fail_extra_struct_field_polars.py", line 56, in <module>
schema.coerce_dataframe(input_df, mode="project_top_level")
data_joinery.schemas.coercion_error.SchemaCoercionError: Schema 'Model': Cannot coerce DataFrame (mode='project_top_level'):
- Additional field 'sub_schema.extra_field': got String. Remove it or use 'project' to drop nested extra fields.
import traceback
from dataclasses import dataclass
from pyspark.sql import DataFrame, SparkSession
from data_joinery import Schema, SchemaCoercionError
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class SubModelWithExtraField:
subfield1: str
subfield2: str
extra_field: str
@dataclass
class ModelWithExtraStructField:
field1: str
field2: str
sub_schema: SubModelWithExtraField
input_schema = Schema(ModelWithExtraStructField)
spark = SparkSession.builder.getOrCreate()
input_df = input_schema.create_dataframe(
[
ModelWithExtraStructField(
field1="value1",
field2="value2",
sub_schema=SubModelWithExtraField(
subfield1="subvalue1", subfield2="subvalue2", extra_field="extra_value"
),
)
],
DataFrame,
session=spark,
)
try:
schema.coerce_dataframe(input_df, mode="project_top_level")
except SchemaCoercionError:
print(traceback.format_exc(limit=1))
Outputs:
Traceback (most recent call last):
File "/home/runner/work/data-joinery/data-joinery/docs_src/learn/coercion/dataclass_coercion_project_top_level_fail_extra_struct_field_spark.py", line 59, in <module>
schema.coerce_dataframe(input_df, mode="project_top_level")
data_joinery.schemas.coercion_error.SchemaCoercionError: Schema 'Model': Cannot coerce DataFrame (mode='project_top_level'):
- Additional field 'sub_schema.extra_field': got StringType(). Remove it or use 'project' to drop nested extra fields.
Project
The mode project projects nested struct fields, removing all fields that are not in
the model:
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class SubModelWithExtraField:
subfield1: str
subfield2: str
extra_field: str
@dataclass
class ModelWithExtraStructField:
field1: str
field2: str
sub_schema: SubModelWithExtraField
input_schema = Schema(ModelWithExtraStructField)
input_df = input_schema.create_dataframe(
[
ModelWithExtraStructField(
field1="value1",
field2="value2",
sub_schema=SubModelWithExtraField(
subfield1="subvalue1", subfield2="subvalue2", extra_field="extra_value"
),
)
],
pl.LazyFrame,
)
coerced_df = schema.coerce_dataframe(input_df, mode="project")
print(coerced_df.collect())
Outputs:
from dataclasses import dataclass
from pyspark.sql import DataFrame, SparkSession
from data_joinery import Schema
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class SubModelWithExtraField:
subfield1: str
subfield2: str
extra_field: str
@dataclass
class ModelWithExtraStructField:
field1: str
field2: str
sub_schema: SubModelWithExtraField
input_schema = Schema(ModelWithExtraStructField)
spark = SparkSession.builder.getOrCreate()
input_df = input_schema.create_dataframe(
[
ModelWithExtraStructField(
field1="value1",
field2="value2",
sub_schema=SubModelWithExtraField(
subfield1="subvalue1", subfield2="subvalue2", extra_field="extra_value"
),
)
],
DataFrame,
session=spark,
)
coerced_df = schema.coerce_dataframe(input_df, mode="project")
coerced_df.show(truncate=50)
Outputs:
Project will still fail if there are missing columns:
import traceback
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema, SchemaCoercionError
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class ModelWithMissingColumn:
field1: str
sub_schema: SubModel
input_schema = Schema(ModelWithMissingColumn)
input_df = input_schema.create_dataframe(
[
ModelWithMissingColumn(
field1="value1",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
)
],
pl.LazyFrame,
)
try:
schema.coerce_dataframe(input_df, mode="project")
except SchemaCoercionError:
print(traceback.format_exc(limit=1))
Outputs:
Traceback (most recent call last):
File "/home/runner/work/data-joinery/data-joinery/docs_src/learn/coercion/dataclass_coercion_project_fail_missing_columns_polars.py", line 45, in <module>
schema.coerce_dataframe(input_df, mode="project")
data_joinery.schemas.coercion_error.SchemaCoercionError: Schema 'Model': Cannot coerce DataFrame (mode='project'):
- Missing field 'field2': expected String. Add the field or update the model.
import traceback
from dataclasses import dataclass
from pyspark.sql import DataFrame, SparkSession
from data_joinery import Schema, SchemaCoercionError
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class ModelWithMissingColumn:
field1: str
sub_schema: SubModel
input_schema = Schema(ModelWithMissingColumn)
spark = SparkSession.builder.getOrCreate()
input_df = input_schema.create_dataframe(
[
ModelWithMissingColumn(
field1="value1",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
)
],
DataFrame,
session=spark,
)
try:
schema.coerce_dataframe(input_df, mode="project")
except SchemaCoercionError:
print(traceback.format_exc(limit=1))
Outputs:
Traceback (most recent call last):
File "/home/runner/work/data-joinery/data-joinery/docs_src/learn/coercion/dataclass_coercion_project_fail_missing_columns_spark.py", line 48, in <module>
schema.coerce_dataframe(input_df, mode="project")
data_joinery.schemas.coercion_error.SchemaCoercionError: Schema 'Model': Cannot coerce DataFrame (mode='project'):
- Missing field 'field2': expected StringType(). Add the field or update the model.
Project Cast
The most relaxed mode, project_cast, recursively projects fields and casts values to the model's field types. This
will still fail if the backend cannot cast a type safely, but this can be useful if reading external data and
you want to easily align your DataFrame with the model's schema. It will also still fail if there are missing columns:
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class ModelWithExtraFieldAndDifferentType:
field1: int
field2: str
sub_schema: SubModel
input_schema = Schema(ModelWithExtraFieldAndDifferentType)
input_df = input_schema.create_dataframe(
[
ModelWithExtraFieldAndDifferentType(
field1=123,
field2="value2",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
)
],
pl.LazyFrame,
)
coerced_df = schema.coerce_dataframe(input_df, mode="project_cast")
print(coerced_df.collect())
Outputs:
from dataclasses import dataclass
from pyspark.sql import DataFrame, SparkSession
from data_joinery import Schema
@dataclass
class SubModel:
subfield1: str
subfield2: str
@dataclass
class Model:
field1: str
field2: str
sub_schema: SubModel
schema = Schema(Model)
@dataclass
class ModelWithExtraFieldAndDifferentType:
field1: int
field2: str
sub_schema: SubModel
input_schema = Schema(ModelWithExtraFieldAndDifferentType)
spark = SparkSession.builder.getOrCreate()
input_df = input_schema.create_dataframe(
[
ModelWithExtraFieldAndDifferentType(
field1=123,
field2="value2",
sub_schema=SubModel(subfield1="subvalue1", subfield2="subvalue2"),
)
],
DataFrame,
session=spark,
)
coerced_df = schema.coerce_dataframe(input_df, mode="project_cast")
coerced_df.show(truncate=50)
Outputs:
Type casting in Data Joinery follows the backend's casting rules. If the cast is permitted then a conversion attempt will happen to try to change the data type. This could still raise a runtime error if the cast is not possible for a specific value in your dataframe.
Schema mismatches such as missing fields fail during coercion. In project_cast mode,
a value that cannot be cast may fail later, when Polars runs the query.