Defining a Schema
Schemas wrap around Pydantic or Python dataclasses. They allow you to define the structure of your data in a backend agnostic way. The same schema can be used to define a schema for any backend you choose.
Dataclasses
Take the below example for a simple dataclass schema:
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema
@dataclass
class Customer:
customer_id: str
name: str
is_active: bool
discount_rate: float
schema = Schema(Customer)
print(schema.native_schema(pl.Schema))
Outputs:
The method native_schema can be used to generate the schema for a specific backend, such as PySpark or Polars.
It doesn't matter what dataclass configuration you choose (frozen, slots, etc.) However if you decide to
use dataclass inheritance it is best practice to use kw_only=True to avoid issues with field ordering:
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema
@dataclass(kw_only=True)
class AddressMixin:
number: str
street: str
postal_code: str
@dataclass(kw_only=True)
class Customer(AddressMixin):
customer_id: str
name: str
schema = Schema(Customer)
print(schema.native_schema(pl.Schema))
Outputs:
from dataclasses import dataclass
from pyspark.sql.types import StructType
from data_joinery import Schema
@dataclass(kw_only=True)
class AddressMixin:
number: str
street: str
postal_code: str
@dataclass(kw_only=True)
class Customer(AddressMixin):
customer_id: str
name: str
schema = Schema(Customer)
print(schema.native_schema(StructType).treeString())
Outputs:
The field order will follow standard dataclass rules
Pydantic models
The same example schema can be defined with Pydantic as well:
import polars as pl
from pydantic import BaseModel, Field
from data_joinery import Schema
class Customer(BaseModel):
customer_id: str
name: str
is_active: bool
discount_rate: float = Field(default=0.0, ge=0.0, le=1.0)
schema = Schema(Customer)
print(schema.native_schema(pl.Schema))
Outputs:
from pydantic import BaseModel, Field
from pyspark.sql.types import StructType
from data_joinery import Schema
class Customer(BaseModel):
customer_id: str
name: str
is_active: bool
discount_rate: float = Field(default=0.0, ge=0.0, le=1.0)
schema = Schema(Customer)
print(schema.native_schema(StructType).treeString())
Outputs:
Pydantic field validation is not enforced
Data Joinery does not enforce Pydantic validation rules. It can be useful to include them in your models for documentation and for generating valid test fixtures, but only the data type is enforced.
Nested schemas
If your data contains nested structs or arrays, describe them with nested dataclasses or Pydantic models:
from dataclasses import dataclass
import polars as pl
from data_joinery import Schema
@dataclass
class Location:
latitude: float
longitude: float
@dataclass
class Employee:
name: str
phone_number: str
@dataclass
class Customer:
customer_id: str
name: str
employees: list[Employee]
location: Location
schema = Schema(Customer)
print(schema.native_schema(pl.Schema))
Outputs:
from dataclasses import dataclass
from pyspark.sql.types import StructType
from data_joinery import Schema
@dataclass
class Location:
latitude: float
longitude: float
@dataclass
class Employee:
name: str
phone_number: str
@dataclass
class Customer:
customer_id: str
name: str
employees: list[Employee]
location: Location
schema = Schema(Customer)
print(schema.native_schema(StructType).treeString())
Outputs:
root
|-- customer_id: string (nullable = true)
|-- name: string (nullable = true)
|-- employees: array (nullable = true)
| |-- element: struct (containsNull = true)
| | |-- name: string (nullable = true)
| | |-- phone_number: string (nullable = true)
|-- location: struct (nullable = true)
| |-- latitude: double (nullable = true)
| |-- longitude: double (nullable = true)