Why do I have a TypeError (Object … is not JSON serializable) when trying to set a session value in a view?

Question:

I have this strange TypeError raised : "Object of type Product is not JSON serializable" when I try to set a session value in a view (in basket app). The error occurs with request.session['Hello'] = 'foo'.

However, this error does not occur elsewhere. For instance, in store app, in views.py, the following request.session['Hello World'] = 'Alloy' works very well.

Why is that happening ?

basket app / views.py

from django.shortcuts import render, get_object_or_404
from django.http import JsonResponse

from . basket import Basket
from store.models import Product
from discount.forms import UserDiscountForm


def basket_summary(request):
    basket = Basket(request)
    context = {'basket':basket}
    request.session['Hello'] = 'foo'
    return render(request,"store/basket_summary.html",context)


def basket_add(request):
    basket = Basket(request)

    if request.POST.get('action') == 'post':
        product_id = int(request.POST.get('productid'))
        product_qty = int(request.POST.get('productqty'))
        product = get_object_or_404(Product, id=product_id)
        basket.add(product=product, qty=product_qty)
        basketqty = basket.__len__()
        response = JsonResponse({'qty':basketqty})
        return response


def basket_add_new(request):
    basket = Basket(request)

    if request.POST.get('action') == 'post':
        product_id = int(request.POST.get('productid'))
        product = get_object_or_404(Product, id=product_id)
        basket.add_new(product=product)
        basketqty = basket.__len__()
        response = JsonResponse({'qty':basketqty})
        return response


def basket_delete(request):
    basket = Basket(request)

    if request.POST.get('action') == 'post':
        product_id = int(request.POST.get('productid'))
        basket.delete(product=product_id)
        basketqty = basket.__len__()
        baskettotal = basket.get_total_price()
        response = JsonResponse({'qty':basketqty, 'subtotal':baskettotal})
        return response


def basket_update(request):
    basket = Basket(request)

    if request.POST.get('action') == 'update-basket':
        product_id = int(request.POST.get('productid'))
        product_qty = int(request.POST.get('productqty'))
        basket.update(product=product_id, qty=product_qty)
        basketqty = basket.__len__()
        baskettotal = basket.get_total_price()
        itemtotal = basket.get_subtotal_price(product=product_id)
        response = JsonResponse({'qty':basketqty, 'baskettotal':baskettotal, 'product_qty':product_qty, 'itemtotal':itemtotal})
        return response

basket app / basket.py

from decimal import Decimal

from store.models import Product


class Basket():
    def __init__(self, request):
        self.session = request.session
        basket = self.session.get('cart')

        if 'cart' not in request.session:
            basket = self.session['cart'] = {}

        self.basket = basket


    def save(self):
        self.session.modified = True


    def add(self, product, qty):
        """
        Adding and updating basket session data
        """
        product_id = str(product.id)
        price = float(product.price)
        subtotal = qty * price

        if product_id not in self.basket:
            self.basket[product_id] = {'price': price, 'qty':int(qty), 'subtotal': subtotal}

        else:
            self.basket[product_id]['qty'] = qty
            self.basket[product_id]['subtotal'] = subtotal

        self.save()


    def add_new(self, product):
        """
        Adding new item in basket session data
        """
        product_id = str(product.id)

        if product_id not in self.basket:
            self.basket[product_id] = {'price': float(product.price), 'qty':1, 'subtotal':float(product.price)}
            # self.basket[product_id] = {'price': float(product.price), 'qty':1}

        else:
            pass

        self.save()


    def __iter__(self):
        """
        Collect the product_id in the session data to query the database and return products
        """
        
        product_ids = self.basket.keys()
        products = Product.objects.filter(id__in=product_ids)
        basket = self.basket.copy()

        for product in products:
            basket[str(product.id)]['product'] = product
    
        for item in basket.values():
            item['price'] = float(item['price'])
            item['total_price'] = item['price'] * item['qty']
    
            yield item


    def __len__(self):
        """
        Get the basket data and count the quantity of all items
        """
            
        return sum(item['qty'] for item in self.basket.values())


    def get_total_price(self):

        return sum(float(item['price']) * item['qty'] for item in self.basket.values())


    def get_subtotal_price(self, product):
        product_id = str(product)

        return self.basket[product_id]['qty'] * self.basket[product_id]['price']



    def delete(self, product):
        """
        Delete item from session data
        """
        product_id = str(product)

        if product_id in self.basket:
            del self.basket[product_id]
        
        self.save()


    def update(self, product, qty):
        """
        Update item in session data
        """
        product_id = str(product)
        
        if product_id in self.basket:
            self.basket[product_id]['qty'] = qty
            self.basket[product_id]['subtotal'] = self.basket[product_id]['qty'] * self.basket[product_id]['price']
        
        self.save()


    def clear(self):
        try:
            del self.session['cart']
        except KeyError:
            pass

        self.save()

store app / models.py

from django.db import models
from django.urls import reverse


class Category(models.Model):
    name = models.CharField(max_length=254, db_index=True)
    slug = models.SlugField(max_length=254, unique = True)


    class Meta:
        verbose_name_plural = 'categories'


    def __str__(self):
        return self.name



class Product(models.Model):
    category = models.ForeignKey(Category, related_name='product', on_delete=models.CASCADE)
    title = models.CharField(max_length=254)
    description = models.TextField(blank=True)
    image = models.ImageField(upload_to='images/', default='images/default.png')
    slug = models.SlugField(max_length=254, unique = True)
    price = models.DecimalField(max_digits=5, decimal_places=2)
    is_active = models.BooleanField(default=True)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)
    image2 = models.ImageField(upload_to='images/', null=True, blank=True)
    image3 = models.ImageField(upload_to='images/', null=True, blank=True)
    stock = models.IntegerField()
    weight = models.IntegerField(verbose_name='Poids (g)')


    class Meta:
        verbose_name_plural = 'products'
        ordering = ('-created', ) # ordering in descending order


    def get_absolute_url(self):
        return reverse('store:product_detail', args=[self.slug])


    def __str__(self):
        return self.title

store app / views.py

from django.shortcuts import get_object_or_404, render
from requests.sessions import session

from .models import Category, Product



def home(request):
    print('----------// HOME PAGE  //----------')
    request.session['Hello World'] = 'Alloy'
    context = {}
    return render(request, 'store/home.html', context)


def categories(request):
    categories = Category.objects.all()
    context = {'categories': categories}
    return render(request, 'store/categories.html', context)


def all_products(request):
    products = Product.objects.all()
    context = {'products': products}
    return render(request, 'store/all_products.html', context)


def product_detail(request, slug):
    product = get_object_or_404(Product, slug=slug, is_active=True)
    context = {'product': product}
    return render(request, 'store/product_details.html', context)

TRACEBACK

Environment:


Request Method: GET
Request URL: http://127.0.0.1:8000/basket/

Django Version: 3.2
Python Version: 3.9.4
Installed Applications:
['django.contrib.admin',
 'django.contrib.auth',
 'django.contrib.contenttypes',
 'django.contrib.sessions',
 'django.contrib.messages',
 'django.contrib.staticfiles',
 'store',
 'account',
 'basket',
 'orders',
 'payment',
 'contact',
 'address',
 'discount',
 'shipping']
Installed Middleware:
['django.middleware.security.SecurityMiddleware',
 'django.contrib.sessions.middleware.SessionMiddleware',
 'django.middleware.common.CommonMiddleware',
 'django.middleware.csrf.CsrfViewMiddleware',
 'django.contrib.auth.middleware.AuthenticationMiddleware',
 'django.contrib.messages.middleware.MessageMiddleware',
 'django.middleware.clickjacking.XFrameOptionsMiddleware']



Traceback (most recent call last):
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocorehandlersexception.py", line 47, in inner
    response = get_response(request)
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangoutilsdeprecation.py", line 119, in __call__
    response = self.process_response(request, response)
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocontribsessionsmiddleware.py", line 61, in process_response
    request.session.save()
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocontribsessionsbackendsdb.py", line 83, in save
    obj = self.create_model_instance(data)
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocontribsessionsbackendsdb.py", line 70, in create_model_instance
    session_data=self.encode(data),
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocontribsessionsbackendsbase.py", line 114, in encode
    return signing.dumps(
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocoresigning.py", line 110, in dumps
    return TimestampSigner(key, salt=salt).sign_object(obj, serializer=serializer, compress=compress)
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocoresigning.py", line 172, in sign_object
    data = serializer().dumps(obj)
  File "C:UsersUtilisateurDocumentsEnvironmentsmonoi_django_virtualenvlibsite-packagesdjangocoresigning.py", line 87, in dumps
    return json.dumps(obj, separators=(',', ':')).encode('latin-1')
  File "c:usersutilisateurappdatalocalprogramspythonpython39libjson__init__.py", line 234, in dumps
    return cls(
  File "c:usersutilisateurappdatalocalprogramspythonpython39libjsonencoder.py", line 199, in encode
    chunks = self.iterencode(o, _one_shot=True)
  File "c:usersutilisateurappdatalocalprogramspythonpython39libjsonencoder.py", line 257, in iterencode
    return _iterencode(o, 0)
  File "c:usersutilisateurappdatalocalprogramspythonpython39libjsonencoder.py", line 179, in default
    raise TypeError(f'Object of type {o.__class__.__name__} '

Exception Type: TypeError at /basket/
Exception Value: Object of type Product is not JSON serializable

Asked By: AlexisLP

||

Answers:

The problem is in the __iter__ method of your Basket class. I believe you iterate over the basket object in the template so it is used in the request (because there is no loop in the view).

Now what is the problem in the method? Well you have this particular line basket = self.basket.copy(), what it does is, that it makes a shallow copy of the dictionary, i.e. the internal objects referred to are the same, but you have a nested dictionary, meaning when you change the nested dictionary you actually change the same dictionary in your basket! You can use copy.deepcopy [Python docs] to make a deep copy of the dictionary:


import copy


def __iter__(self):
    ...
    basket = copy.deepcopy(self.basket)
    ...

Answered By: Abdul Aziz Barkat

Abdul, You are genius. Your solution and explanation worked for me like a charm. Good answer

Answered By: sdanquah