Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 19 additions & 18 deletions kolibri/core/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,10 @@ class BaseValuesViewset(viewsets.GenericViewSet):
# Cached validation schema for DEBUG mode: (expected_fields, nested_schemas)
# Built once during _ensure_initialized to avoid per-request recomputation.
_validation_schema = None
# Whether this class derives its values from serializer_class. Legacy
# explicit-values viewsets may pair a write-oriented serializer with a
# different read shape, so DEBUG output validation only applies when True.
_serializer_derived = False
# Whether _ensure_initialized has run for this class
_initialized = False
# Guards _ensure_initialized for this class; each subclass gets its own
Expand Down Expand Up @@ -312,6 +316,7 @@ def _do_initialize(cls):

has_explicit_values = isinstance(getattr(cls, "values", None), tuple)
serializer_class = getattr(cls, "serializer_class", None)
cls._serializer_derived = not has_explicit_values

if has_explicit_values:
cls._values = tuple(cls.values)
Expand Down Expand Up @@ -350,16 +355,12 @@ def _do_initialize(cls):
if queryset is not None and hasattr(queryset, "model"):
cls._pk_getter = operator.itemgetter(queryset.model._meta.pk.name)

# Cache validation schema for DEBUG mode
if settings.DEBUG:
serializer = cls._get_own("_cached_serializer")
if (
serializer is None
and getattr(cls, "serializer_class", None) is not None
):
serializer = cls.serializer_class()
if serializer is not None:
cls._validation_schema = cls._build_validation_schema(serializer)
# Cache validation schema for DEBUG mode — serializer-derived
# viewsets only; the serializer is the read contract for them.
if settings.DEBUG and cls._serializer_derived:
cls._validation_schema = cls._build_validation_schema(
cls._cached_serializer
)

cls._initialized = True

Expand Down Expand Up @@ -610,19 +611,19 @@ def _validate_output(self, items: List[Dict[str, Any]]) -> None:

Uses the cached _validation_schema when available (built during
_ensure_initialized), falling back to building from the serializer.

Only applies to serializer-derived viewsets — legacy explicit-values
viewsets may pair a write-oriented serializer with a different read
shape, so the serializer is not their read contract.
"""
if not items:
if not items or not self._serializer_derived:
return

schema = self._validation_schema
if schema is None:
# Fallback for viewsets without a cached schema
if self._cached_serializer is not None:
schema = self._build_validation_schema(self._cached_serializer)
elif self.serializer_class is not None:
schema = self._build_validation_schema(self.serializer_class())
else:
return
# Class was initialized under DEBUG=False; build from the
# serializer cached during derivation.
schema = self._build_validation_schema(self._cached_serializer)

self._validate_items_against_schema(items, schema)

Expand Down
30 changes: 30 additions & 0 deletions kolibri/core/test/test_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -1483,6 +1483,36 @@ def consolidate(self, items, queryset):
result = _serialize(V(), [{"id": "a1"}])
self.assertEqual(result[0]["book_titles"], ["B1", "B2"])

@override_settings(DEBUG=True)
def test_explicit_values_viewset_skips_output_validation(self):
"""Legacy explicit-values viewsets often pair a write-oriented
serializer_class with a different read shape — DEBUG output
validation must not apply to them."""
Ser = make_serializer(id=serializers.CharField(), name=serializers.CharField())

class V(BaseValuesViewset, ListModelMixin):
queryset = Author.objects.none()
serializer_class = Ser
values = ("id",)

result = _serialize(V(), [{"id": "a1"}])
self.assertEqual(result[0], {"id": "a1"})

def test_explicit_values_viewset_skips_validation_fallback(self):
"""Even with no cached schema (class initialized under DEBUG=False),
the runtime fallback must not validate explicit-values viewsets."""
Ser = make_serializer(id=serializers.CharField(), name=serializers.CharField())

class V(BaseValuesViewset, ListModelMixin):
queryset = Author.objects.none()
serializer_class = Ser
values = ("id",)

viewset = V() # initialized with DEBUG=False — no cached schema
with override_settings(DEBUG=True):
result = _serialize(viewset, [{"id": "a1"}])
self.assertEqual(result[0], {"id": "a1"})

@override_settings(DEBUG=False)
def test_validation_skipped_when_debug_false(self):
"""DEBUG=False — drifting output passes silently."""
Expand Down
Loading