Schema Generation
Karrio uses strongly-typed data structures to ensure data integrity and improve the developer experience. Schema generation converts a carrier’s API specification into Python data classes that represent the exact structure of requests and responses for the carrier’s API.
Overview
The schema generation process involves:
- Prepare Schema Files: Create JSON or XML files with API examples
- Configure Generation Script: Set up the
generate
script with correct parameters - Run Generation: Execute
./bin/run-generate-on [path]
to generate Python classes - Use Generated Types: Import and use the generated classes in your provider implementation
Step 1: Prepare Schema Files
Create schema files in the schemas/
directory of your extension based on the carrier’s API documentation.
For JSON APIs
Create JSON files with actual API request/response examples (not JSON Schema definitions):
1schemas/ 2├── error_response.json # Error response format 3├── rate_request.json # Rate request format 4├── rate_response.json # Rate response format 5├── shipment_request.json # Shipment request format 6├── shipment_response.json # Shipment response format 7├── tracking_request.json # Tracking request format 8└── tracking_response.json # Tracking response format
Example rate_request.json:
1{ 2 "shipper": { 3 "addressLine1": "123 Main St", 4 "city": "New York", 5 "postalCode": "10001", 6 "countryCode": "US" 7 }, 8 "recipient": { 9 "addressLine1": "456 Oak Ave", 10 "city": "Los Angeles", 11 "postalCode": "90210", 12 "countryCode": "US" 13 }, 14 "packages": [ 15 { 16 "weight": 5.0, 17 "weightUnit": "KG", 18 "dimensions": { 19 "length": 10.0, 20 "width": 10.0, 21 "height": 10.0, 22 "unit": "CM" 23 } 24 } 25 ] 26}
For XML/SOAP APIs
Create XSD schema files or XML examples:
1schemas/ 2├── error_response.xsd 3├── rate_request.xsd 4├── rate_response.xsd 5└── ...
Note: Use actual API samples from the carrier’s documentation, not abstract schema definitions.
Step 2: Configure the Generate Script
The CLI automatically creates a generate
script in your extension root. You need to configure it with the correct parameters for your carrier’s API.
Understanding Generation Parameters
The key parameters that affect how Python classes are generated:
Parameter | Usage | API Field Format | Example |
---|---|---|---|
--nice-property-names | Convert to snake_case | APIs with camelCase fields | Easyship |
--no-nice-property-names | Preserve original naming | APIs with camelCase that should stay camelCase | UPS, FedEx, SEKO |
--no-append-type-suffix --no-nice-property-names | For PascalCase APIs | APIs with PascalCase fields | Some enterprise APIs |
Example Generate Script
File: modules/connectors/[carrier_name]/generate
1#!/bin/bash 2set -e 3 4SCHEMAS=./schemas 5LIB_MODULES=./karrio/schemas/[carrier_name] 6ROOT="$(pwd)" 7 8# Clean existing generated files 9find "${LIB_MODULES}" -name "*.py" -exec rm -r {} \; 10touch "${LIB_MODULES}/__init__.py" 11 12# Generation function 13generate_schema() { 14 echo "Generating $1..." 15 "${ROOT}/bin/cli" codegen generate "$1" "$2" --no-nice-property-names 16} 17 18# Generate all required schemas 19generate_schema "${SCHEMAS}/error_response.json" "${LIB_MODULES}/error_response.py" 20generate_schema "${SCHEMAS}/rate_request.json" "${LIB_MODULES}/rate_request.py" 21generate_schema "${SCHEMAS}/rate_response.json" "${LIB_MODULES}/rate_response.py" 22generate_schema "${SCHEMAS}/shipment_request.json" "${LIB_MODULES}/shipment_request.py" 23generate_schema "${SCHEMAS}/shipment_response.json" "${LIB_MODULES}/shipment_response.py" 24generate_schema "${SCHEMAS}/tracking_request.json" "${LIB_MODULES}/tracking_request.py" 25generate_schema "${SCHEMAS}/tracking_response.json" "${LIB_MODULES}/tracking_response.py"
Choose Correct Parameters
For camelCase APIs (UPS, FedEx, SEKO):
1"${ROOT}/bin/cli" codegen generate "$1" "$2" --no-nice-property-names
For snake_case APIs (Easyship):
1"${ROOT}/bin/cli" codegen generate "$1" "$2" --nice-property-names
For PascalCase APIs:
1"${ROOT}/bin/cli" codegen generate "$1" "$2" --no-append-type-suffix --no-nice-property-names
🚨 CRITICAL RULES BEFORE GENERATION:
-
Generated Files Are READ-ONLY:
- NEVER edit files in
karrio/schemas/[carrier_name]/
- Generated files are completely overwritten on each run
- Any manual changes will be lost permanently
- NEVER edit files in
-
Source-Only Editing:
- Only modify files in
schemas/
directory (your source files) - To change API structure: Edit source schemas → regenerate
- Never create schema files manually outside the CLI
- Only modify files in
-
Generation Must Succeed:
- If generation fails, STOP IMMEDIATELY
- Fix the source schema files or CLI parameters
- Never proceed with incomplete/broken generation
-
Verification Required:
- Always test imports after generation
- Verify class names match expectations
- Ensure type annotations are correct
Step 3: Run Schema Generation
Execute the generation command from the project root:
Activate environment first (CRITICAL)1source ./bin/activate-env 2 3# Generate schemas for your carrier 4./bin/run-generate-on modules/connectors/[carrier_name] 5 6# For hub carriers 7./bin/run-generate-on community/plugins/[carrier_name] 8 9# Make the script executable if needed 10chmod +x modules/connectors/[carrier_name]/generate
This command:
- Finds the
generate
script in your extension - Executes it to process all schema files
- Creates Python classes in
karrio/schemas/[carrier_name]/
Step 4: Verify Generated Code
After successful generation, verify the output:
Check generated files1ls -la modules/connectors/[carrier_name]/karrio/schemas/[carrier_name]/ 2 3# Test imports 4python -c " 5import karrio.schemas.[carrier_name].rate_request as req 6import karrio.schemas.[carrier_name].rate_response as res 7print('Schema imports successful') 8"
Generated Code Structure
The generated Python classes use attrs
and jstruct
decorators for type safety and serialization:
Example Generated Class:
karrio/schemas/[carrier_name]/rate_request.py1import attr 2import jstruct 3import typing 4 5@attr.s(auto_attribs=True) 6class Address: 7 addressLine1: typing.Optional[str] = None 8 city: typing.Optional[str] = None 9 postalCode: typing.Optional[str] = None 10 countryCode: typing.Optional[str] = None 11 12@attr.s(auto_attribs=True) 13class Package: 14 weight: typing.Optional[float] = None 15 weightUnit: typing.Optional[str] = None 16 dimensions: typing.Optional[Dimensions] = None 17 18@attr.s(auto_attribs=True) 19class RateRequestType: 20 shipper: typing.Optional[Address] = None 21 recipient: typing.Optional[Address] = None 22 packages: typing.Optional[typing.List[Package]] = jstruct.JList[Package]
Class Naming Conventions:
- With default settings: Classes have
Type
suffix (e.g.,RateRequestType
) - With
--no-append-type-suffix
: Classes named exactly as in schema (e.g.,RateRequest
)
Step 5: Using Generated Types
Import and use the generated classes in your provider implementation:
In Rate Implementation:
karrio/providers/[carrier_name]/rate.py1import karrio.schemas.[carrier_name].rate_request as carrier_req 2import karrio.schemas.[carrier_name].rate_response as carrier_res 3 4def rate_request(payload: models.RateRequest, settings: Settings) -> lib.Serializable: 5 # Use generated request type 6 request = carrier_req.RateRequestType( 7 shipper=carrier_req.Address( 8 addressLine1=payload.shipper.address_line1, 9 city=payload.shipper.city, 10 # ... map other fields 11 ), 12 recipient=carrier_req.Address( 13 addressLine1=payload.recipient.address_line1, 14 city=payload.recipient.city, 15 # ... map other fields 16 ), 17 packages=[ 18 carrier_req.Package( 19 weight=package.weight.value, 20 weightUnit=package.weight.unit, 21 # ... map other fields 22 ) 23 for package in packages 24 ] 25 ) 26 27 return lib.Serializable(request, lib.to_dict) 28 29def parse_rate_response(response: lib.Deserializable, settings: Settings): 30 data = response.deserialize() 31 32 # Use generated response type 33 rate_response = lib.to_object(carrier_res.RateResponseType, data) 34 35 # Extract rates using typed access 36 rates = [ 37 models.RateDetails( 38 carrier_id=settings.carrier_id, 39 service=rate.serviceCode, 40 total_charge=lib.to_money(rate.totalCharge), 41 currency=rate.currency, 42 ) 43 for rate in (rate_response.rates or []) 44 ] 45 46 return rates, []
Advanced CLI Tools
Create Tree Command
Generate initialization templates for complex nested objects:
Generate a tree structure for a class1kcli codegen tree --module=karrio.schemas.[carrier_name].rate_request --class-name=RateRequestType --module-alias=carrier 2 3# Output example: 4carrier.RateRequestType( 5 shipper=carrier.Address( 6 addressLine1=None, 7 city=None, 8 postalCode=None, 9 countryCode=None, 10 ), 11 recipient=carrier.Address( 12 addressLine1=None, 13 city=None, 14 postalCode=None, 15 countryCode=None, 16 ), 17 packages=[ 18 carrier.Package( 19 weight=None, 20 weightUnit=None, 21 dimensions=carrier.Dimensions( 22 length=None, 23 width=None, 24 height=None, 25 unit=None, 26 ), 27 ) 28 ], 29)
This output can be copied directly into your mapping functions as a starting template.
Troubleshooting
Common Issues
Generation Fails:
- Ensure schema files contain valid JSON/XML
- Check that
generate
script is executable:chmod +x generate
- Verify environment is activated:
source ./bin/activate-env
Import Errors:
- Verify extension is installed:
pip install -e modules/connectors/[carrier_name]
- Check
__init__.py
files exist in schema directories - Ensure class names match generated output
Wrong Field Names:
- Review generation parameters in your
generate
script - Use
--nice-property-names
for snake_case conversion - Use
--no-nice-property-names
to preserve original naming
Testing Generation
Test your generation setup:
Test CLI command directly1./bin/cli codegen generate schemas/rate_request.json test_output.py --no-nice-property-names 2 3# Check output 4cat test_output.py
Best Practices
- Use Real API Examples: Create schema files from actual carrier API documentation
- Choose Correct Parameters: Match generation parameters to your API’s field format
- Test Imports Early: Verify generated classes import correctly before implementing logic
- Use Generated Types: Always import and use generated classes in provider implementations
- Regenerate When Needed: Only modify source schema files, never edit generated Python files
XML API Considerations
For XML/SOAP APIs, the process is similar but uses generateDS
instead:
XML generation in generate script1generateDS --no-namespace-defs -o "${LIB_MODULES}/rate_request.py" $SCHEMAS/rate_request.xsd
Generated XML classes have different patterns:
- No
Type
suffix by default - Use
lib.to_element
for deserialization - Use
request.serialize()
for XML serialization
Testing Generated Types
MANDATORY: Validate your generated types work correctly in tests:
1. Import Verification Test
1def test_schema_imports(self): 2 """Verify all generated schema types import correctly.""" 3 try: 4 import karrio.schemas.[carrier_name].rate_request as req 5 import karrio.schemas.[carrier_name].rate_response as res 6 print("All schema imports successful") 7 self.assertTrue(hasattr(req, 'RateRequestType')) # Adjust for your naming 8 self.assertTrue(hasattr(res, 'RateResponseType')) 9 except ImportError as e: 10 self.fail(f"Schema import failed: {e}")
2. Type Validation Test
1def test_generated_types_usage(self): 2 """Verify generated types work in mapping functions.""" 3 request = gateway.mapper.create_rate_request(self.RateRequest) 4 5 # Verify the request uses generated types correctly 6 serialized = request.serialize() 7 self.assertIsInstance(serialized, dict) # For JSON APIs 8 9 # Verify specific required fields exist 10 self.assertIn('shipper', serialized) 11 self.assertIn('recipient', serialized) 12 self.assertIn('packages', serialized)
3. Schema Regeneration Test
Always verify that schema regeneration still works:
Test regeneration doesn't break1./bin/run-generate-on modules/connectors/[carrier_name] 2python -c "import karrio.schemas.[carrier_name] as schemas; print('Regeneration successful')"
Next Steps
Once schema generation is complete:
- Configure Metadata: Set up connection settings and data units
- Implement API Requests: Create the HTTP communication layer
- Implement Data Mapping: Transform between Karrio and carrier formats
- Write Tests: Validate your integration works correctly with exact patterns