Semantic Scalars Reference
Built-in Scalar Types — Explore the full library of pre-built scalars
Custom scalar types let you create domain-specific types that carry validation logic into your GraphQL schema. FraiseQL’s Python SDK is compile-time only — your @scalar class is exported to schema.json and the Rust runtime handles the schema from there. This guide shows you how to design and implement custom scalars correctly.
FraiseQL ships with a large library of built-in semantic scalars (Email, PhoneNumber, URL, UUID, DateTime, and many more). Use those when they fit.
Write a CustomScalar subclass when you need:
| Scenario | Solution |
|---|---|
| Standard email address | Use built-in Email scalar from fraiseql.scalars |
| Email with custom domain allowlist | CustomScalar subclass with domain check in parse_value |
| Standard UUID | Use built-in UUID scalar from fraiseql.scalars |
| Custom range-bounded integer | CustomScalar subclass |
| ISO 8601 datetime | Use built-in DateTime scalar |
All custom scalars must subclass CustomScalar and implement three instance methods:
serialize(self, value) — convert an internal value to its GraphQL wire formatparse_value(self, value) — validate and convert a client-supplied variable valueparse_literal(self, ast) — validate and convert a hardcoded literal in a GraphQL queryThe class must also declare a name class attribute — this becomes the scalar name in the compiled schema.
import fraiseqlfrom fraiseql import CustomScalar, scalar
@scalarclass EmailAddress(CustomScalar): """Email address with basic RFC 5322 format validation."""
name = "EmailAddress"
def serialize(self, value): result = str(value).lower() if "@" not in result: raise ValueError(f"Cannot serialize invalid email: {value!r}") return result
def parse_value(self, value): if not isinstance(value, str): raise ValueError("Email must be a string") if "@" not in value or "." not in value.split("@")[-1]: raise ValueError(f"Invalid email format: {value!r}") return value.lower()
def parse_literal(self, ast): if hasattr(ast, "value"): return self.parse_value(ast.value) raise ValueError("Expected a string literal for EmailAddress")"""Email address with basic RFC 5322 format validation."""scalar EmailAddressOnce registered, use the class as a type annotation inside @fraiseql.type:
import fraiseqlfrom fraiseql.scalars import ID
@fraiseql.typeclass User: id: ID email: EmailAddress # registered custom scalar name: strThe compiled schema references EmailAddress as a custom scalar, and the Rust runtime enforces the validation contract at query/mutation boundaries.
from fraiseql import CustomScalar, scalar
@scalarclass PositiveInt(CustomScalar): """An integer greater than zero."""
name = "PositiveInt"
def serialize(self, value): n = int(value) if n <= 0: raise ValueError(f"PositiveInt must be > 0, got {n}") return n
def parse_value(self, value): if not isinstance(value, int) or isinstance(value, bool): raise ValueError("PositiveInt must be an integer") if value <= 0: raise ValueError(f"PositiveInt must be > 0, got {value}") return value
def parse_literal(self, ast): if hasattr(ast, "value"): return self.parse_value(int(ast.value)) raise ValueError("Expected an integer literal for PositiveInt")from fraiseql import CustomScalar, scalar
@scalarclass USDPrice(CustomScalar): """Price in USD. Must be between $0.01 and $999,999.99."""
name = "USDPrice"
def serialize(self, value): amount = round(float(value), 2) if not (0.01 <= amount <= 999_999.99): raise ValueError(f"USDPrice out of range: {amount}") return amount
def parse_value(self, value): try: amount = round(float(value), 2) except (TypeError, ValueError) as e: raise ValueError(f"USDPrice must be a number, got {value!r}") from e if not (0.01 <= amount <= 999_999.99): raise ValueError(f"USDPrice must be between $0.01 and $999,999.99") return amount
def parse_literal(self, ast): if hasattr(ast, "value"): return self.parse_value(ast.value) raise ValueError("Expected a numeric literal for USDPrice")import refrom fraiseql import CustomScalar, scalar
_E164_RE = re.compile(r"^\+[1-9]\d{1,14}$")
@scalarclass E164Phone(CustomScalar): """Phone number in E.164 format (e.g. +12025551234)."""
name = "E164Phone"
def serialize(self, value): s = str(value) if not _E164_RE.match(s): raise ValueError(f"Cannot serialize invalid E.164 phone: {s!r}") return s
def parse_value(self, value): if not isinstance(value, str): raise ValueError("E164Phone must be a string") if not _E164_RE.match(value): raise ValueError( f"E164Phone must match +[country][number] format, got {value!r}" ) return value
def parse_literal(self, ast): if hasattr(ast, "value"): return self.parse_value(ast.value) raise ValueError("Expected a string literal for E164Phone")For common domain types, FraiseQL provides pre-defined scalars in fraiseql.scalars. These are NewType aliases — import them directly and use as type annotations without writing a CustomScalar subclass.
import fraiseqlfrom fraiseql.scalars import ID, Email, PhoneNumber, URL, DateTime, UUID, Decimal
@fraiseql.typeclass Contact: id: ID email: Email phone: PhoneNumber | None website: URL | None created_at: DateTimeThe available built-in scalars include: ID, UUID, DateTime, Date, Time, Json, Decimal, Vector, Email, PhoneNumber, URL, Slug, Markdown, HTML, IPAddress, CurrencyCode, Money, LTree, and many more. See the Semantic Scalars Reference for the full list.
After defining your types and scalars, export the schema for compilation:
import fraiseql
# ... define @fraiseql.type, @fraiseql.query, @fraiseql.scalar classes ...
fraiseql.export_schema("schema.json")Then compile with the CLI:
fraiseql compileThe compiler validates all custom scalar references and emits schema.compiled.json for the Rust runtime.
Use fraiseql.validators.validate_custom_scalar to unit-test your scalar logic directly, without standing up a server.
import pytestfrom fraiseql.validators import validate_custom_scalar, ScalarValidationErrorfrom myapp.scalars import EmailAddress, USDPrice
class TestEmailAddressScalar: def test_valid_email_parse_value(self): result = validate_custom_scalar(EmailAddress, "User@Example.com") assert result == "user@example.com"
def test_missing_at_sign_raises(self): with pytest.raises(ScalarValidationError): validate_custom_scalar(EmailAddress, "notanemail")
def test_serialize_lowercases(self): result = validate_custom_scalar(EmailAddress, "Admin@Example.COM", context="serialize") assert result == "admin@example.com"
class TestUSDPriceScalar: def test_valid_price(self): result = validate_custom_scalar(USDPrice, 9.99) assert result == 9.99
def test_zero_price_raises(self): with pytest.raises(ScalarValidationError): validate_custom_scalar(USDPrice, 0.0)
def test_too_large_raises(self): with pytest.raises(ScalarValidationError): validate_custom_scalar(USDPrice, 1_000_000.00)The validate_custom_scalar signature:
def validate_custom_scalar( scalar_class: type[CustomScalar], value: Any, context: str = "parse_value", # "serialize" | "parse_value" | "parse_literal") -> Any: ...ScalarValidationError carries the scalar name, context, and message for precise failure reporting.
Define each scalar in a dedicated module (e.g. myapp/scalars.py) and import that module before calling export_schema:
from fraiseql import CustomScalar, scalarimport re
@scalarclass EmailAddress(CustomScalar): name = "EmailAddress" # ... methods ...
@scalarclass USDPrice(CustomScalar): name = "USDPrice" # ... methods ...import fraiseqlimport myapp.scalars # ensures all @scalar decorators run before export
from fraiseql.scalars import IDfrom myapp.scalars import EmailAddress, USDPrice
@fraiseql.typeclass Product: id: ID name: str price: USDPrice
fraiseql.export_schema("schema.json")Understanding the architecture prevents common mistakes:
Python source @scalar class EmailAddress(CustomScalar) ↓ fraiseql.export_schema("schema.json") ↓ schema.json ← contains customScalars: { "EmailAddress": { ... } } ↓ fraiseql compile ↓ schema.compiled.json ↓ Rust runtime (handles all GraphQL requests, enforces scalars)There are no Python resolvers, no async def handlers, and no runtime FFI. The Python SDK is a schema authoring tool only.
Custom scalars map to Protobuf types for the gRPC transport. See the gRPC Transport page for the full GraphQL-to-Protobuf type mapping table.
Semantic Scalars Reference
Built-in Scalar Types — Explore the full library of pre-built scalars
Type System Guide
Type System — How to use scalars inside @fraiseql.type
TOML Configuration
Configuration Reference — Compiler and runtime configuration