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:

  1. Detects the missing reference
  2. Fetches the document from Sanity
  3. Creates the Django record
  4. Returns the newly created instance

2. Protection Mechanisms

Inherited from ReferenceResolver and BaseSyncHandler:

ProtectionDescriptionDefault
Circular DetectionPrevents A→B→A reference cyclesAutomatic
Depth LimitingMaximum auto-fetch depth5 levels
Timeout ProtectionAuto-fetch timeout15 seconds
Celery RetryExponential backoff on failure3 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:

ParameterTypeDescription
reference_dataDict or strSanity reference dict with _ref key, or raw ref ID string
target_modelType[Model]Django model class to resolve to
field_namestrOptional field name for logging
requiredboolIf 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:

ParameterTypeDescription
reference_arrayList[Dict]List of Sanity reference dicts
target_modelType[Model]Django model class to resolve to
field_namestrOptional 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:

LevelMessage
DEBUGSuccessful resolutions with IDs
INFOPartial array resolutions (e.g., "Resolved 3/5 references")
WARNINGResolution failures, invalid reference types
ERRORCritical 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:

  1. Uses Global ReferenceResolver - Shares state with domain sync handlers
  2. Respects SyncContext - Aware of webhook processing context
  3. Triggers Domain Handlers - Auto-fetch uses domain-specific sync handlers
  4. Audit Logging - Auto-fetched records are logged in SyncAuditLog

Performance Considerations

When Auto-Fetch Occurs

ScenarioBehavior
Reference exists in DjangoReturn immediately (no fetch)
Reference missing, exists in SanityFetch, create, return
Reference missing from bothReturn None or raise ValueError

Optimization Tips

  1. Batch Webhooks - Process webhooks in dependency order when possible
  2. Pre-create Common References - Ensure stores, brands, categories exist before products
  3. Use Array Resolution - resolve_fk_array() batches log messages

Was this page helpful?