Repository: vitalik/django-ninja

Issue:
[BUG] "ValidationError" and "HttpError" are not unpickable
**Describe the bug**

Hi.

It seems the `ValidationError` can be pickled but not un-pickled:

```python
import pickle
from ninja.errors import ValidationError

error = ValidationError(errors=[])

serialized = pickle.dumps(error)  # OK
deserialized = pickle.loads(serialized)  # NOK
```

It causes:

```
Traceback (most recent call last):
  File "/home/delgan/test.py", line 6, in <module>
    deserialized = pickle.loads(serialized)
                   ^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: ValidationError.__init__() missing 1 required positional argument: 'errors'
```

This is actually a reccurent problem while inheriting Python `Exception`:
- [Cannot unpickle Exception subclass](https://stackoverflow.com/questions/41808912/cannot-unpickle-exception-subclass)
- [How to pickle inherited exceptions?](https://stackoverflow.com/questions/49715881/how-to-pickle-inherited-exceptions)
-  [How to make a custom exception class with multiple init args pickleable](https://stackoverflow.com/questions/16244923/how-to-make-a-custom-exception-class-with-multiple-init-args-pickleable)

The fix requires to implement a `__reduce__` method for these classes.

The `multiprocessing` module makes extensive use of `pickle` protocol, and having objects that can't be de-serialized is source of unexpected errors (in my case, because the `Exception` is sent to a background processing thread for logging).

**Versions (please complete the following information):**
 - Python version: 3.11.3
 - Django version: 4.2.4
 - Django-Ninja version: 0.22.2
 - Pydantic version: 0.4.4


Comments:

Patch:
diff --git a/ninja/errors.py b/ninja/errors.py
index 1be1dff..3c1056a 100644
--- a/ninja/errors.py
+++ b/ninja/errors.py
@@ -39,14 +39,18 @@ class ValidationError(Exception):
     """
 
     def __init__(self, errors: List[DictStrAny]) -> None:
-        super().__init__()
         self.errors = errors
+        super().__init__(errors)
 
 
 class HttpError(Exception):
     def __init__(self, status_code: int, message: str) -> None:
         self.status_code = status_code
-        super().__init__(message)
+        self.message = message
+        super().__init__(status_code, message)
+
+    def __str__(self) -> str:
+        return self.message
 
 
 def set_default_exc_handlers(api: "NinjaAPI") -> None:


Test patch:
diff --git a/tests/test_errors.py b/tests/test_errors.py
new file mode 100644
index 0000000..b19c015
--- /dev/null
+++ b/tests/test_errors.py
@@ -0,0 +1,26 @@
+import pickle
+
+from ninja.errors import HttpError, ValidationError
+
+
+def test_validation_error_is_picklable_and_unpicklable():
+    error_to_serialize = ValidationError([{"testkey": "testvalue"}])
+
+    serialized = pickle.dumps(error_to_serialize)
+    assert serialized  # Not empty
+
+    deserialized = pickle.loads(serialized)
+    assert isinstance(deserialized, ValidationError)
+    assert deserialized.errors == error_to_serialize.errors
+
+
+def test_http_error_is_picklable_and_unpicklable():
+    error_to_serialize = HttpError(500, "Test error")
+
+    serialized = pickle.dumps(error_to_serialize)
+    assert serialized  # Not empty
+
+    deserialized = pickle.loads(serialized)
+    assert isinstance(deserialized, HttpError)
+    assert deserialized.status_code == error_to_serialize.status_code
+    assert deserialized.message == error_to_serialize.message