Skip to content

Scalar Types

FraiseQL provides scalar types that map between Python, GraphQL, and PostgreSQL. This reference covers all available scalars and their usage.

from fraiseql.scalars import (
# Core types
ID, UUID, DateTime, Date, Time, Decimal, Json, Vector,
# Contact/Communication
Email, PhoneNumber, URL, DomainName, Hostname,
# Location/Address
PostalCode, Latitude, Longitude, Coordinates, Timezone,
LocaleCode, LanguageCode, CountryCode,
# Financial
IBAN, CUSIP, ISIN, SEDOL, LEI, MIC, CurrencyCode,
Money, ExchangeCode, ExchangeRate, StockSymbol, Percentage,
# Identifiers
Slug, SemanticVersion, HashSHA256, APIKey,
LicensePlate, VIN, TrackingNumber, ContainerNumber,
# Networking
IPAddress, IPv4, IPv6, MACAddress, CIDR, Port,
# Transportation
AirportCode, PortCode, FlightNumber,
# Content
Markdown, HTML, MimeType, Color, Image, File,
# Database
LTree, DateRange, Duration,
)

FraiseQL provides 47+ domain-specific scalar types with built-in validation, organized into logical categories.

The primary identifier type. Maps to PostgreSQL UUID.

from fraiseql.scalars import ID
@fraiseql.type
class User:
id: ID # UUID v4
AspectValue
Pythonstr
GraphQLID
PostgreSQLUUID
FormatUUID v4 (e.g., 550e8400-e29b-41d4-a716-446655440000)

Usage:

@fraiseql.query(sql_source="v_user")
def user(id: ID) -> User | None:
pass
@fraiseql.mutation(sql_source="fn_create_user", operation="CREATE")
def create_user(name: str) -> User:
# Returns entity with generated ID
pass

Explicit UUID type. Identical to ID but semantically clearer.

from fraiseql.scalars import UUID
@fraiseql.type
class Session:
session_id: UUID
user_id: UUID
AspectValue
Pythonstr
GraphQLUUID
PostgreSQLUUID

Built-in Python str. Maps to PostgreSQL text types.

@fraiseql.type
class Post:
title: str # TEXT
slug: str # TEXT
content: str # TEXT
AspectValue
Pythonstr
GraphQLString
PostgreSQLTEXT, VARCHAR

Built-in Python int. Maps to PostgreSQL integers.

@fraiseql.type
class Product:
quantity: int # INTEGER
view_count: int # INTEGER
AspectValue
Pythonint
GraphQLInt
PostgreSQLINTEGER, BIGINT, SMALLINT

Built-in Python float. Maps to PostgreSQL floating-point.

@fraiseql.type
class Location:
latitude: float # DOUBLE PRECISION
longitude: float
AspectValue
Pythonfloat
GraphQLFloat
PostgreSQLREAL, DOUBLE PRECISION

Built-in Python bool.

@fraiseql.type
class User:
is_active: bool
is_verified: bool
AspectValue
Pythonbool
GraphQLBoolean
PostgreSQLBOOLEAN

Timestamp with timezone. ISO 8601 format.

from fraiseql.scalars import DateTime
@fraiseql.type
class Event:
created_at: DateTime
updated_at: DateTime
scheduled_for: DateTime | None
AspectValue
Pythonstr (ISO 8601)
GraphQLDateTime
PostgreSQLTIMESTAMPTZ
Format2024-01-15T10:30:00Z

Example values:

{
"created_at": "2024-01-15T10:30:00Z",
"updated_at": "2024-01-15T14:22:33.123456Z"
}

Date without time component.

from fraiseql.scalars import Date
@fraiseql.type
class Invoice:
issue_date: Date
due_date: Date
AspectValue
Pythonstr (ISO 8601)
GraphQLDate
PostgreSQLDATE
Format2024-01-15

Time without date component.

from fraiseql.scalars import Time
@fraiseql.type
class Schedule:
start_time: Time
end_time: Time
AspectValue
Pythonstr (ISO 8601)
GraphQLTime
PostgreSQLTIME, TIMETZ
Format14:30:00

Arbitrary-precision decimal. Use for financial calculations.

from fraiseql.scalars import Decimal
@fraiseql.type
class Order:
subtotal: Decimal
tax: Decimal
total: Decimal
AspectValue
Pythonstr (to preserve precision)
GraphQLDecimal
PostgreSQLNUMERIC, DECIMAL
Format"123.45"

Why string representation?

JSON and JavaScript cannot represent arbitrary precision decimals. FraiseQL serializes as strings to preserve exact values:

{
"total": "1234567890.12345678901234567890"
}

PostgreSQL definition:

total DECIMAL(12, 2) NOT NULL -- 12 digits, 2 decimal places

Arbitrary JSON data. Maps to PostgreSQL JSONB.

from fraiseql.scalars import Json
@fraiseql.type
class User:
preferences: Json # Arbitrary JSON object
metadata: Json | None
AspectValue
Pythondict, list, or primitive
GraphQLJSON
PostgreSQLJSONB

Example values:

{
"preferences": {
"theme": "dark",
"notifications": {
"email": true,
"push": false
}
},
"metadata": null
}

Use cases:

  • User preferences/settings
  • Flexible metadata
  • External API responses
  • Configuration objects

Vector embeddings for ML/AI applications. Maps to pgvector.

from fraiseql.scalars import Vector
@fraiseql.type
class Document:
content: str
embedding: Vector # 1536-dimensional vector
AspectValue
Pythonlist[float]
GraphQL[Float!]
PostgreSQLvector (pgvector extension)

PostgreSQL setup:

CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE tb_document (
pk_document INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL,
content TEXT NOT NULL,
embedding vector(1536) -- OpenAI ada-002 dimension
);
-- Similarity search index
CREATE INDEX idx_document_embedding ON tb_document
USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);

Use Python list[T] for array types:

@fraiseql.type
class Post:
tags: list[str] # TEXT[]
categories: list[int] # INTEGER[]
scores: list[float] # DOUBLE PRECISION[]
PythonGraphQLPostgreSQL
list[str][String!]TEXT[]
list[int][Int!]INTEGER[]
list[float][Float!]DOUBLE PRECISION[]
list[ID][ID!]UUID[]

Nullable arrays vs nullable elements:

tags: list[str] # Non-null array, non-null elements
tags: list[str] | None # Nullable array, non-null elements
tags: list[str | None] # Non-null array, nullable elements

Use union with None for nullable fields:

@fraiseql.type
class User:
email: str # Required
bio: str | None # Optional (nullable)
avatar_url: str | None # Optional (nullable)

GraphQL output:

type User {
email: String!
bio: String
avatarUrl: String
}

FraiseQL automatically coerces PostgreSQL types:

PostgreSQLPython Scalar
UUIDID, UUID
TEXT, VARCHAR, CHARstr
INTEGER, BIGINT, SMALLINTint
REAL, DOUBLE PRECISIONfloat
BOOLEANbool
TIMESTAMPTZ, TIMESTAMPDateTime
DATEDate
TIME, TIMETZTime
NUMERIC, DECIMALDecimal
JSONB, JSONJson
vectorVector
TEXT[], VARCHAR[]list[str]
INTEGER[]list[int]
UUID[]list[ID]

RFC 5322 validated email address with domain-aware filtering.

from fraiseql.scalars import Email
@fraiseql.type
class User:
email: Email
backup_email: Email | None
AspectValue
GraphQLEmail
PostgreSQLTEXT
ValidationRFC 5322 format

Filter Operators:

OperatorDescriptionExample
_domain_eqMatch domain exactly"acme.com"
_domain_inDomain in list["acme.com", "corp.com"]
_domain_endswithDomain suffix".edu"
_is_freemailGmail, Yahoo, etc.true
_is_corporateNot freemailtrue
_local_startswithLocal part prefix"sales."
query {
# Corporate emails only
leads(where: { email: { _is_corporate: true } }) { id email }
# All .edu domains
students(where: { email: { _domain_endswith: ".edu" } }) { id email }
}

E.164 format phone numbers with geographic filtering.

from fraiseql.scalars import PhoneNumber
@fraiseql.type
class Contact:
phone: PhoneNumber
mobile: PhoneNumber | None
AspectValue
GraphQLPhoneNumber
PostgreSQLTEXT
FormatE.164 (e.g., +14155551234)

Filter Operators:

OperatorDescriptionExample
_country_code_eqCountry code"+1", "+44"
_country_eqCountry"US", "GB"
_region_eqGeographic region"Europe"
_is_mobileMobile numbertrue
_is_toll_freeToll-free numbertrue
query {
# US mobile numbers
contacts(where: {
phone: { _country_code_eq: "+1", _is_mobile: true }
}) { id phone }
}

RFC 3986 validated URLs with component extraction.

from fraiseql.scalars import URL
@fraiseql.type
class Website:
homepage: URL
favicon: URL | None

Filter Operators:

OperatorDescriptionExample
_domain_eqMatch domain"github.com"
_protocol_eqProtocol"https"
_is_secureHTTPS onlytrue
_path_startswithPath prefix"/api/"
_tld_eqTop-level domain"io"
from fraiseql.scalars import DomainName, Hostname
@fraiseql.type
class Server:
domain: DomainName # e.g., "example.com"
hostname: Hostname # e.g., "api.example.com"

Filter Operators: _tld_eq, _subdomain_of, _depth_eq, _endswith

from fraiseql.scalars import Latitude, Longitude, Coordinates
@fraiseql.type
class Location:
lat: Latitude # -90 to 90
lng: Longitude # -180 to 180
coords: Coordinates # Combined lat,lng or GeoJSON
TypeRangeExample
Latitude-90 to 9040.7128
Longitude-180 to 180-74.0060
CoordinatesGeoJSON or pair{"lat": 40.7, "lng": -74.0}

Coordinates Filter Operators (PostGIS):

OperatorDescriptionExample
_within_radiusWithin distance{ lat, lng, radius_km }
_within_boundsIn bounding box{ min_lat, max_lat, min_lng, max_lng }
_distance_from_ltCloser than{ lat, lng, km: 50 }
_in_countryWithin country"US"
query {
# Find stores within 50km of NYC
stores(where: {
location: {
_within_radius: { lat: 40.7128, lng: -74.006, radius_km: 50 }
}
}) { id name location }
}
from fraiseql.scalars import PostalCode, CountryCode, Timezone
@fraiseql.type
class Address:
postal_code: PostalCode # ZIP/postal codes
country: CountryCode # ISO 3166-1 alpha-2 (e.g., "US")
timezone: Timezone # IANA timezone (e.g., "America/New_York")

CountryCode Filter Operators:

OperatorDescriptionExample
_continent_eqContinent"EU", "AS", "NA"
_in_euEU member statetrue
_in_eurozoneUses Eurotrue
_gdpr_applicableGDPR appliestrue
_in_g20G20 economytrue
query {
# Find EU customers (GDPR-applicable)
customers(where: { country: { _gdpr_applicable: true } }) {
id name country
}
}

PostalCode Filter Operators: _startswith, _zip5_eq (US), _area_eq (UK), _fsa_eq (CA)

Timezone Filter Operators: _offset_eq, _observes_dst, _continent_eq

from fraiseql.scalars import LocaleCode, LanguageCode
@fraiseql.type
class UserPreferences:
locale: LocaleCode # e.g., "en-US"
language: LanguageCode # ISO 639-1 (e.g., "en")

LanguageCode Filter Operators: _family_eq, _script_eq, _is_rtl

from fraiseql.scalars import IBAN, CUSIP, ISIN, SEDOL, LEI
@fraiseql.type
class BankAccount:
iban: IBAN # International Bank Account Number
@fraiseql.type
class Security:
cusip: CUSIP | None # North American securities
isin: ISIN | None # International Securities ID
sedol: SEDOL | None # London Stock Exchange
lei: LEI | None # Legal Entity Identifier
TypeFormatExample
IBANISO 13616DE89370400440532013000
CUSIP9 characters037833100
ISIN12 charactersUS0378331005
SEDOL7 characters2046251
LEI20 characters529900W18LQJJN6SJ336

IBAN Filter Operators:

OperatorDescriptionExample
_country_eqCountry code (first 2 chars)"DE", "FR"
_bank_code_eqBank identifier"COBADEFF"
_is_sepaSEPA zone accounttrue
_is_validPasses mod-97 checktrue
query {
# Find German SEPA accounts
accounts(where: {
iban: { _country_eq: "DE", _is_sepa: true }
}) { id iban holder_name }
}

ISIN/CUSIP Filter Operators: _country_eq, _issuer_startswith, _is_equity

LEI Filter Operators: _lou_eq, _is_active

from fraiseql.scalars import CurrencyCode, Money, ExchangeRate, StockSymbol
@fraiseql.type
class Transaction:
amount: Money # Amount with currency
currency: CurrencyCode # ISO 4217 (e.g., "USD")
exchange_rate: ExchangeRate | None
@fraiseql.type
class Stock:
symbol: StockSymbol # e.g., "AAPL"
exchange: ExchangeCode # e.g., "NYSE"

CurrencyCode Filter Operators:

OperatorDescriptionExample
_is_fiatFiat currencytrue
_is_cryptoCryptocurrencytrue
_is_majorG10 currenciestrue
_is_peggedPegged currencytrue
_decimals_eqMinor unit decimals2 (USD), 0 (JPY)

Money Filter Operators:

OperatorDescriptionExample
_amount_gteAmount greater than or equal"10000.00"
_amount_lteAmount less than or equal"50000.00"
_currency_eqCurrency match"USD"
_converted_gteConverted amount greater than or equal"10000.00" (in target currency)
query {
# High-value USD transactions
transactions(where: {
total: { _amount_gte: "10000", _currency_eq: "USD" }
}) { id total }
}
from fraiseql.scalars import Percentage
@fraiseql.type
class TaxRate:
rate: Percentage # 0-100 or 0-1 (configurable)
discount: Percentage | None

Filter Operators: _gt, _gte, _lt, _lte, _is_zero, _is_positive

from fraiseql.scalars import VIN, LicensePlate
@fraiseql.type
class Vehicle:
vin: VIN # 17-character Vehicle Identification Number
license_plate: LicensePlate
TypeFormatExample
VIN17 characters1HGBH41JXMN109186
LicensePlateRegional formatABC-1234

VIN Filter Operators:

OperatorDescriptionExample
_wmi_eqWorld Manufacturer ID (first 3)"WVW" (VW Germany)
_manufacturer_eqManufacturer name"Toyota"
_manufacturer_inManufacturers["Toyota", "Honda"]
_model_year_eqModel year2024
_model_year_gteModel year (gte)2020
_country_eqCountry of manufacture"DE", "JP"
_region_eqManufacturing region"EUROPE", "ASIA"
query {
# Find 2020+ German vehicles
vehicles(where: {
vin: { _model_year_gte: 2020, _region_eq: "EUROPE" }
}) { id vin manufacturer model_year }
}
from fraiseql.scalars import TrackingNumber, ContainerNumber
@fraiseql.type
class Shipment:
tracking: TrackingNumber # Carrier tracking number
container: ContainerNumber | None # ISO 6346

TrackingNumber Filter Operators:

OperatorDescriptionExample
_carrier_eqCarrier"FEDEX", "UPS", "DHL"
_carrier_inCarriers["FEDEX", "UPS"]
_service_type_eqService level"EXPRESS", "GROUND"
query {
# Find express FedEx shipments
shipments(where: {
tracking: { _carrier_eq: "FEDEX", _service_type_eq: "EXPRESS" }
}) { id tracking status }
}

ContainerNumber Filter Operators: _owner_eq, _category_eq, _size_eq, _type_eq

from fraiseql.scalars import Slug, SemanticVersion, HashSHA256, APIKey
@fraiseql.type
class Package:
slug: Slug # URL-safe identifier
version: SemanticVersion # e.g., "1.2.3"
@fraiseql.type
class Integration:
api_key: APIKey
checksum: HashSHA256

SemanticVersion Filter Operators:

OperatorDescriptionExample
_major_eqMajor version2
_minor_gteMinor version (gte)5
_satisfiesnpm/cargo range"^1.2.0", "~2.0"
_is_stableNo prerelease tagtrue
_has_prereleaseHas prereleasetrue
_gteSemver (gte)"1.0.0"
query {
# Find compatible versions
packages(where: { version: { _satisfies: "^2.0.0" } }) {
id name version
}
# Find stable releases only
releases(where: { version: { _is_stable: true } }) {
id version published_at
}
}

Slug Filter Operators: _startswith, _contains, _path_startswith, _path_depth_eq

APIKey Filter Operators: _prefix_eq, _is_live, _is_test, _is_secret

from fraiseql.scalars import IPAddress, IPv4, IPv6, CIDR
@fraiseql.type
class Server:
ip: IPAddress # IPv4 or IPv6
ipv4: IPv4 | None # IPv4 only
ipv6: IPv6 | None # IPv6 only
subnet: CIDR # CIDR notation
TypeExample
IPv4192.168.1.1
IPv62001:0db8:85a3::8a2e:0370:7334
CIDR192.168.0.0/24

IPAddress Filter Operators:

OperatorDescriptionExample
_in_subnetWithin CIDR range"10.0.0.0/8"
_is_ipv4IPv4 addresstrue
_is_ipv6IPv6 addresstrue
_is_privateRFC 1918 privatetrue
_is_publicPublic IPtrue
_is_loopbackLoopback addresstrue
_is_multicastMulticast addresstrue
query {
# Find requests from private IPs
requests(where: { ip: { _is_private: true } }) {
id ip path
}
# Find requests from specific subnet
requests(where: { ip: { _in_subnet: "192.168.0.0/16" } }) {
id ip
}
}

CIDR Filter Operators: _contains (IP), _overlaps (CIDR)

from fraiseql.scalars import MACAddress, Port
@fraiseql.type
class NetworkDevice:
mac: MACAddress # e.g., "00:1A:2B:3C:4D:5E"
port: Port # 0-65535

Port Filter Operators:

OperatorDescriptionExample
_is_well_knownPorts 0-1023true
_is_privilegedPorts (lt) 1024true
_is_httpHTTP ports (80, 8080)true
_is_httpsHTTPS port (443)true
_is_databaseDB ports (3306, 5432, etc.)true
_gtePort (gte)8000
query {
# Find database services
services(where: { port: { _is_database: true } }) {
id name port protocol
}
}
from fraiseql.scalars import AirportCode, FlightNumber, PortCode
@fraiseql.type
class Flight:
departure: AirportCode # IATA code (e.g., "JFK")
arrival: AirportCode
flight_number: FlightNumber # e.g., "AA123"
@fraiseql.type
class ShippingRoute:
origin_port: PortCode # UN/LOCODE
destination_port: PortCode

AirportCode Filter Operators:

OperatorDescriptionExample
_country_eqCountry"US"
_continent_eqContinent"EU"
_is_hubMajor airline hubtrue
_size_eqAirport size"LARGE_HUB"
_within_radiusNear location{ lat, lng, radius_km }
query {
# Find US hub airports
airports(where: {
code: { _country_eq: "US", _is_hub: true }
}) { code name city }
# Find airports near NYC
airports(where: {
code: { _within_radius: { lat: 40.7, lng: -74.0, radius_km: 100 } }
}) { code name }
}

FlightNumber Filter Operators:

OperatorDescriptionExample
_airline_eqIATA airline code"AA", "UA"
_airline_inAirlines["AA", "UA", "DL"]
_flight_gteFlight number (gte)1000
_airline_alliance_eqAlliance"ONEWORLD", "STAR_ALLIANCE"

PortCode Filter Operators: _country_eq, _type_eq (SEA, RIVER), _is_seaport

from fraiseql.scalars import Markdown, HTML
@fraiseql.type
class Article:
content: Markdown # Markdown-formatted
rendered: HTML | None # HTML-formatted

Filter Operators: _contains, _search (fulltext), _has_headings, _has_code_blocks

from fraiseql.scalars import MimeType, Color, Image, File
@fraiseql.type
class Asset:
mime_type: MimeType # e.g., "image/png"
color: Color | None # Hex, RGB, or named
@fraiseql.type
class Attachment:
image: Image | None # URL or base64
file: File | None # URL or path

MimeType Filter Operators:

OperatorDescriptionExample
_type_eqPrimary type"image", "video"
_subtype_eqSubtype"png", "mp4"
_is_imageImage typetrue
_is_videoVideo typetrue
_is_documentDocument typetrue
_is_binaryBinary contenttrue
query {
# Find image files
files(where: { mime_type: { _is_image: true } }) {
id name mime_type
}
}

Color Filter Operators:

OperatorDescriptionExample
_is_darkDark colortrue
_is_lightLight colortrue
_is_grayscaleGrayscaletrue
_hue_gteHue range (0-360)180
_saturation_gteSaturation (0-100)50
query {
# Find dark theme assets
assets(where: { color: { _is_dark: true } }) {
id name color
}
}
from fraiseql.scalars import LTree
@fraiseql.type
class Category:
path: LTree # ltree path (e.g., "root.electronics.phones")

LTree Filter Operators:

OperatorDescriptionExample
_ancestor_ofIs ancestor of"root.electronics.phones"
_descendant_ofIs descendant of"root.electronics"
_nlevel_eqExact depth3
_nlevel_gteDepth (gte)2
_matches_lqueryLQuery pattern"root.*.phones"
query {
# Find all electronics subcategories
categories(where: {
path: { _descendant_of: "root.electronics" }
}) { id name path }
# Find top-level categories
categories(where: { path: { _nlevel_eq: 2 } }) {
id name path
}
}
from fraiseql.scalars import DateRange, Duration
@fraiseql.type
class Booking:
period: DateRange # e.g., "[2024-01-01,2024-01-15)"
duration: Duration # ISO 8601 (e.g., "P1Y2M3D")

DateRange Filter Operators:

OperatorDescriptionExample
_contains_dateRange contains date"2024-06-15"
_overlapsRanges overlap"[2024-06-01,2024-06-30)"
_adjacentRanges are adjacent"[2024-05-01,2024-06-01)"
_strictly_leftEntirely before"[2024-07-01,)"
_strictly_rightEntirely after"(,2024-05-01]"
query {
# Find bookings that include a specific date
bookings(where: {
period: { _contains_date: "2024-06-15" }
}) { id room period }
# Find overlapping reservations
reservations(where: {
dates: { _overlaps: "[2024-06-10,2024-06-20)" }
}) { id guest dates }
}

Duration Filter Operators:

OperatorDescriptionExample
_gtDuration (gt)"PT1H" (1 hour)
_lteDuration (lte)"PT30M" (30 min)
_total_hours_gtTotal hours (gt)1.5
_total_minutes_ltTotal minutes (lt)30
query {
# Find long videos
videos(where: { duration: { _total_hours_gt: 1 } }) {
id title duration
}
}

Every rich type has specialized filter operators tailored to its domain. Below is a quick reference—see Query Operators for complete documentation.

CategoryTypeKey Operators
ContactEmail_domain_eq, _domain_endswith, _is_freemail, _is_corporate
PhoneNumber_country_code_eq, _is_mobile, _region_eq
URL_domain_eq, _protocol_eq, _path_startswith, _is_secure
GeographyCountryCode_continent_eq, _in_eu, _gdpr_applicable, _in_g20
Coordinates_within_radius, _within_bounds, _distance_from_lt
PostalCode_startswith, _zip5_eq, _area_eq
FinancialCurrencyCode_is_fiat, _is_major, _is_crypto
Money_amount_gte, _currency_eq, _converted_gt
IBAN_country_eq, _bank_code_eq, _is_sepa
IdentifiersVIN_wmi_eq, _manufacturer_eq, _model_year_gte
SemanticVersion_major_eq, _satisfies, _is_stable
TrackingNumber_carrier_eq, _service_type_eq
TransportationAirportCode_country_eq, _is_hub, _within_radius
FlightNumber_airline_eq, _airline_alliance_eq
ContentMimeType_is_image, _is_document, _type_eq
Color_is_dark, _hue_gte, _is_grayscale
NetworkingIPAddress_in_subnet, _is_private, _is_ipv6
Port_is_privileged, _is_database
DatabaseLTree_ancestor_of, _descendant_of, _nlevel_eq
DateRange_contains_date, _overlaps, _adjacent
query {
# EU customers with corporate emails
customers(where: {
country: { _in_eu: true }
email: { _is_corporate: true }
}) {
id name email country
}
}
query {
# Stores within 50km of NYC
stores(where: {
location: {
_within_radius: { lat: 40.7128, lng: -74.006, radius_km: 50 }
}
}) {
id name location
}
}
query {
# High-value transactions in major currencies
transactions(where: {
amount: { _amount_gte: "10000", _currency_in: ["USD", "EUR", "GBP"] }
}) {
id amount currency
}
}

All rich scalars store as TEXT in PostgreSQL with application-level validation:

CategoryTypesPostgreSQL
ContactEmail, PhoneNumber, URLTEXT
LocationCoordinates, PostalCodeTEXT
FinancialIBAN, CUSIP, MoneyTEXT
IdentifiersVIN, TrackingNumberTEXT
NetworkingIPAddress, CIDRTEXT (or INET/CIDR)
TransportationAirportCode, FlightNumberTEXT

This design provides:

  • Flexibility: Validation rules can change without migrations
  • Portability: Standard TEXT columns work everywhere
  • Performance: TEXT indexing is well-optimized
  • Separation: Validation logic in application, storage in database

Define your own domain-specific scalars:

from typing import NewType
import fraiseql
# Simple custom scalar (zero runtime overhead)
CompanyId = NewType("CompanyId", str)
ProductSKU = NewType("ProductSKU", str)
@fraiseql.type
class Order:
id: str
company_id: CompanyId # serializes as String, documents intent
sku: ProductSKU

For scalars with custom validation:

@fraiseql.scalar
class TaxId:
"""Tax identification number with validation."""
@staticmethod
def serialize(value: str) -> str:
return value.upper().replace("-", "")
@staticmethod
def parse(value: str) -> str:
cleaned = value.upper().replace("-", "")
if len(cleaned) != 9:
raise ValueError("Tax ID must be 9 characters")
return cleaned

In CQRS views, scalars are serialized into JSONB:

CREATE VIEW v_order AS
SELECT
o.id,
jsonb_build_object(
'id', o.id::text, -- ID as string
'total', o.total::text, -- Decimal as string
'created_at', o.created_at, -- DateTime as ISO 8601
'items', o.items, -- Json as-is
'tags', to_jsonb(o.tags) -- Array as JSON array
) AS data
FROM tb_order o;
import fraiseql
from fraiseql.scalars import ID, DateTime, Date, Decimal, Json, Vector
@fraiseql.type
class Product:
"""E-commerce product with all scalar types."""
id: ID
sku: str
name: str
description: str | None
price: Decimal
quantity: int
weight: float | None
is_available: bool
release_date: Date | None
created_at: DateTime
updated_at: DateTime
tags: list[str]
categories: list[int]
metadata: Json | None
embedding: Vector | None
@fraiseql.query(sql_source="v_product")
def product(id: ID) -> Product | None:
pass
@fraiseql.query(sql_source="v_product")
def products(
is_available: bool | None = None,
min_price: Decimal | None = None,
max_price: Decimal | None = None,
tags: list[str] | None = None,
limit: int = 20
) -> list[Product]:
pass

Semantic Scalars

Semantic Scalars — Advanced types with rich filtering operators

Operators

Operators — Filter operators for queries