Integrations - Serializer Reference Resolver
Location: api/nextango/apps/integrations/sync/services/serializer_reference_resolver.py
Last Updated: 2026-01-25
Overview
The SerializerReferenceResolver provides a clean interface for DRF serializers to resolve Sanity references with automatic fetching of missing references. When a reference exists in Sanity but not yet in Django, the resolver automatically fetches and creates the record.
This is particularly useful during webhook processing where referenced documents may arrive out of order.
Architecture
┌─────────────────────────────────────────────────────────────┐
│ DRF Serializer │
│ (e.g., ProductSerializer) │
└──────────────────────┬──────────────────────────────────────┘
│ resolve_fk_reference()
│ resolve_fk_array()
┌──────────────────────▼──────────────────────────────────────┐
│ SerializerReferenceResolver │
│ (Wrapper Layer) │
│ - Clean API for serializers │
│ - Error handling with required flag │
│ - Logging of resolution results │
└──────────────────────┬──────────────────────────────────────┘
│ resolve_sanity_reference()
┌──────────────────────▼──────────────────────────────────────┐
│ ReferenceResolver │
│ (Core Resolution Engine) │
│ - Circular reference detection │
│ - Auto-fetch from Sanity │
│ - Depth limiting (MAX_AUTO_FETCH_DEPTH = 5) │
└──────────────────────┬──────────────────────────────────────┘
│ auto_fetch_callback()
┌──────────────────────▼──────────────────────────────────────┐
│ Domain Sync Handlers │
│ (ProductSyncHandler, StoreSyncHandler, etc.) │
│ - Fetch from Sanity API │
│ - Create Django record │
│ - Handle nested references │
└─────────────────────────────────────────────────────────────┘
Key Features
1. Automatic Reference Fetching
When a reference doesn't exist in Django but exists in Sanity, the resolver:
- Detects the missing reference
- Fetches the document from Sanity
- Creates the Django record
- Returns the newly created instance
2. Protection Mechanisms
Inherited from ReferenceResolver and BaseSyncHandler:
| Protection | Description | Default |
|---|---|---|
| Circular Detection | Prevents A→B→A reference cycles | Automatic |
| Depth Limiting | Maximum auto-fetch depth | 5 levels |
| Timeout Protection | Auto-fetch timeout | 15 seconds |
| Celery Retry | Exponential backoff on failure | 3 retries |
3. Singleton Pattern
Uses a global singleton to ensure consistent state across serializer instances:
from nextango.apps.integrations.sync.services.serializer_reference_resolver import (
get_serializer_reference_resolver
)
resolver = get_serializer_reference_resolver()
API Reference
SerializerReferenceResolver Class
Source: serializer_reference_resolver.py:15-122
resolve_fk_reference()
Resolve a single Sanity reference to a Django model instance.
Signature:
def resolve_fk_reference(
self,
reference_data: Dict[str, Any],
target_model: Type[models.Model],
field_name: str = None,
required: bool = False
) -> Optional[models.Model]
Parameters:
| Parameter | Type | Description |
|---|---|---|
reference_data | Dict or str | Sanity reference dict with _ref key, or raw ref ID string |
target_model | Type[Model] | Django model class to resolve to |
field_name | str | Optional field name for logging |
required | bool | If True, raises ValueError when resolution fails |
Returns: Django model instance or None if not found (when required=False)
Raises: ValueError if required=True and reference cannot be resolved
Example:
from nextango.apps.integrations.sync.services.serializer_reference_resolver import (
get_serializer_reference_resolver
)
from nextango.apps.stores.models import Store
resolver = get_serializer_reference_resolver()
# From webhook payload
store_ref = {"_ref": "store-abc123", "_type": "reference"}
# Resolve - auto-fetches if not in DB
store = resolver.resolve_fk_reference(
reference_data=store_ref,
target_model=Store,
field_name="store",
required=True # Raises if store can't be resolved
)
resolve_fk_array()
Resolve an array of Sanity references, filtering out failures.
Signature:
def resolve_fk_array(
self,
reference_array: List[Dict[str, Any]],
target_model: Type[models.Model],
field_name: str = None
) -> List[models.Model]
Parameters:
| Parameter | Type | Description |
|---|---|---|
reference_array | List[Dict] | List of Sanity reference dicts |
target_model | Type[Model] | Django model class to resolve to |
field_name | str | Optional field name for logging |
Returns: List of resolved Django model instances (excludes failures)
Example:
from nextango.apps.products.models import Category
# From webhook payload - product belongs to multiple categories
category_refs = [
{"_ref": "category-electronics", "_type": "reference"},
{"_ref": "category-sale", "_type": "reference"},
{"_ref": "category-new-arrivals", "_type": "reference"}
]
# Resolve all - auto-fetches missing ones
categories = resolver.resolve_fk_array(
reference_array=category_refs,
target_model=Category,
field_name="categories"
)
# Logs: "Resolved 3/3 references for categories" (or partial if some failed)
Global Accessor Function
get_serializer_reference_resolver()
Source: serializer_reference_resolver.py:128-134
Get the global singleton instance.
def get_serializer_reference_resolver() -> SerializerReferenceResolver:
"""Get the global serializer reference resolver singleton."""
global _serializer_resolver
if _serializer_resolver is None:
_serializer_resolver = SerializerReferenceResolver()
logger.debug("Created serializer reference resolver singleton")
return _serializer_resolver
Usage in Serializers
Pattern: Webhook Serializer with Auto-Fetch
from rest_framework import serializers
from nextango.apps.integrations.sync.services.serializer_reference_resolver import (
get_serializer_reference_resolver
)
from nextango.apps.products.models import Product, ProductVariant
from nextango.apps.stores.models import Store
class ProductWebhookSerializer(serializers.Serializer):
"""Process product webhook with auto-fetch for missing references."""
_id = serializers.CharField()
title = serializers.CharField()
store = serializers.DictField(required=False) # Sanity reference
categories = serializers.ListField(required=False) # Array of references
def create(self, validated_data):
resolver = get_serializer_reference_resolver()
# Resolve store reference - auto-fetches if missing
store = resolver.resolve_fk_reference(
reference_data=validated_data.get('store'),
target_model=Store,
field_name='store',
required=True
)
# Resolve category references - filters out failures
categories = resolver.resolve_fk_array(
reference_array=validated_data.get('categories', []),
target_model=Category,
field_name='categories'
)
product = Product.objects.create(
sanity_id=validated_data['_id'],
title=validated_data['title'],
store=store
)
product.categories.set(categories)
return product
Error Handling
Required References
When required=True, the resolver raises ValueError if resolution fails:
try:
store = resolver.resolve_fk_reference(
reference_data=store_ref,
target_model=Store,
field_name='store',
required=True
)
except ValueError as e:
# Handle missing required reference
logger.error(f"Required reference failed: {e}")
raise serializers.ValidationError({"store": str(e)})
Optional References
When required=False (default), returns None on failure:
brand = resolver.resolve_fk_reference(
reference_data=brand_ref,
target_model=Brand,
field_name='brand',
required=False # Default
)
if brand:
product.brand = brand
# If None, brand stays null (optional field)
Logging
The resolver logs at multiple levels:
| Level | Message |
|---|---|
DEBUG | Successful resolutions with IDs |
INFO | Partial array resolutions (e.g., "Resolved 3/5 references") |
WARNING | Resolution failures, invalid reference types |
ERROR | Critical failures during auto-fetch |
Example log output:
DEBUG Resolved store: store-abc123 -> Store pk=42
INFO Resolved 3/5 references for categories
WARNING Could not resolve brand reference 'brand-deleted': Record not found in Sanity
Integration with Sync Framework
The SerializerReferenceResolver integrates with the broader sync framework:
- Uses Global ReferenceResolver - Shares state with domain sync handlers
- Respects SyncContext - Aware of webhook processing context
- Triggers Domain Handlers - Auto-fetch uses domain-specific sync handlers
- Audit Logging - Auto-fetched records are logged in
SyncAuditLog
Performance Considerations
When Auto-Fetch Occurs
| Scenario | Behavior |
|---|---|
| Reference exists in Django | Return immediately (no fetch) |
| Reference missing, exists in Sanity | Fetch, create, return |
| Reference missing from both | Return None or raise ValueError |
Optimization Tips
- Batch Webhooks - Process webhooks in dependency order when possible
- Pre-create Common References - Ensure stores, brands, categories exist before products
- Use Array Resolution -
resolve_fk_array()batches log messages
Related Documentation
- Reference Resolver - Core resolution logic
- Services - Universal sync framework
- Adapters - Domain-specific sync adapters
- Views - Webhook receiver pipeline