Skip to content

Commit 07d51e8

Browse files
committed
Support for a default hook.
1 parent c30559f commit 07d51e8

File tree

4 files changed

+67
-19
lines changed

4 files changed

+67
-19
lines changed

tests/test_serialization.py

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,23 @@ def test_serialize_unknown():
1313
object we can't handle.
1414
"""
1515
with pytest.raises(TypeError):
16-
yyjson.Document({
17-
"example": ClassThatCantBeSerialized()
18-
})
16+
yyjson.Document({"example": ClassThatCantBeSerialized()})
1917

2018
with pytest.raises(TypeError):
2119
yyjson.Document([ClassThatCantBeSerialized()])
20+
21+
22+
def test_serialize_default_func():
23+
"""
24+
Ensure that we can serialize an object by providing a default function.
25+
"""
26+
27+
def default(obj):
28+
if isinstance(obj, ClassThatCantBeSerialized):
29+
return "I'm a string now!"
30+
raise TypeError(f"Can't serialize {obj}")
31+
32+
doc = yyjson.Document(
33+
{"example": ClassThatCantBeSerialized()}, default=default
34+
)
35+
assert doc.as_obj["example"] == "I'm a string now!"

yyjson/__init__.pyi

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import enum
2-
from typing import Any, Optional, List, Dict, Union
2+
from pathlib import Path
3+
from typing import Any, Optional, List, Dict, Union, Callable
34

45
class ReaderFlags(enum.IntFlag):
56
STOP_WHEN_DONE = 0x02
@@ -15,15 +16,22 @@ class WriterFlags(enum.IntFlag):
1516
ALLOW_INF_AND_NAN = 0x08
1617
INF_AND_NAN_AS_NULL = 0x10
1718

18-
Content = Union[str, bytes, List, Dict]
19+
Content = Union[str, bytes, List, Dict, Path]
1920

2021
class Document:
2122
as_obj: Any
22-
def __init__(self, content: Content, flags: Optional[ReaderFlags] = ...): ...
23+
def __init__(
24+
self,
25+
content: Content,
26+
flags: Optional[ReaderFlags] = ...,
27+
default: Callable[[Any], Any] = ...,
28+
): ...
2329
def __len__(self) -> int: ...
2430
def get_pointer(self, pointer: str) -> Any: ...
2531
def dumps(
26-
self, flags: Optional[WriterFlags] = ..., at_pointer: Optional[str] = ...
32+
self,
33+
flags: Optional[WriterFlags] = ...,
34+
at_pointer: Optional[str] = ...,
2735
) -> str: ...
2836
def patch(
2937
self,

yyjson/document.c

Lines changed: 36 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -283,10 +283,22 @@ PyTypeObject *type_for_conversion(PyObject *obj) {
283283
* Recursively convert a Python object into yyjson elements.
284284
*/
285285
static inline yyjson_mut_val *mut_primitive_to_element(
286-
yyjson_mut_doc *doc, PyObject *obj
286+
DocumentObject *self,
287+
yyjson_mut_doc *doc,
288+
PyObject *obj
287289
) {
288290
const PyTypeObject *ob_type = type_for_conversion(obj);
289291

292+
if (yyjson_unlikely(ob_type == NULL) && self->default_func != NULL) {
293+
PyObject *result = PyObject_CallFunctionObjArgs(self->default_func, obj, NULL);
294+
if (result == NULL) {
295+
return NULL;
296+
}
297+
yyjson_mut_val *val = mut_primitive_to_element(self, doc, result);
298+
Py_DECREF(result);
299+
return val;
300+
}
301+
290302
if (ob_type == &PyUnicode_Type) {
291303
Py_ssize_t str_len;
292304
const char *str = PyUnicode_AsUTF8AndSize(obj, &str_len);
@@ -321,7 +333,7 @@ static inline yyjson_mut_val *mut_primitive_to_element(
321333
yyjson_mut_val *val = yyjson_mut_arr(doc);
322334
yyjson_mut_val *object_value = NULL;
323335
for (Py_ssize_t i = 0; i < PyList_GET_SIZE(obj); i++) {
324-
object_value = mut_primitive_to_element(doc, PyList_GET_ITEM(obj, i));
336+
object_value = mut_primitive_to_element(self, doc, PyList_GET_ITEM(obj, i));
325337

326338
if (yyjson_unlikely(object_value == NULL)) {
327339
return NULL;
@@ -339,7 +351,7 @@ static inline yyjson_mut_val *mut_primitive_to_element(
339351
while (PyDict_Next(obj, &i, &key, &value)) {
340352
Py_ssize_t str_len;
341353
const char *str = PyUnicode_AsUTF8AndSize(key, &str_len);
342-
object_value = mut_primitive_to_element(doc, value);
354+
object_value = mut_primitive_to_element(self, doc, value);
343355
if (yyjson_unlikely(object_value == NULL)) {
344356
return NULL;
345357
}
@@ -359,11 +371,9 @@ static inline yyjson_mut_val *mut_primitive_to_element(
359371
} else if (obj == Py_None) {
360372
return yyjson_mut_null(doc);
361373
} else {
362-
PyErr_SetString(
363-
PyExc_TypeError,
364-
// TODO: We can provide a much better error here. Also add support
365-
// for a default hook.
366-
"Tried to serialize an object we don't know how to handle."
374+
PyErr_Format(PyExc_TypeError,
375+
"Object of type '%s' is not JSON serializable",
376+
Py_TYPE(obj)->tp_name
367377
);
368378
return NULL;
369379
}
@@ -372,6 +382,7 @@ static inline yyjson_mut_val *mut_primitive_to_element(
372382
static void Document_dealloc(DocumentObject *self) {
373383
if (self->i_doc != NULL) yyjson_doc_free(self->i_doc);
374384
if (self->m_doc != NULL) yyjson_mut_doc_free(self->m_doc);
385+
Py_XDECREF(self->default_func);
375386
Py_TYPE(self)->tp_free((PyObject *)self);
376387
}
377388

@@ -426,20 +437,33 @@ PyDoc_STRVAR(
426437
":param content: The initial content of the document.\n"
427438
":type content: ``str``, ``bytes``, ``Path``, ``dict``, ``list``\n"
428439
":param flags: Flags that modify the document parsing behaviour.\n"
429-
":type flags: :class:`ReaderFlags`, optional"
440+
":type flags: :class:`ReaderFlags`, optional\n"
441+
":param default: A function called to convert objects that are not\n"
442+
" JSON serializable. Should return a JSON serializable version\n"
443+
" of the object or raise a TypeError.\n"
444+
":type default: callable, optional"
430445
);
431446
static int Document_init(DocumentObject *self, PyObject *args, PyObject *kwds) {
432-
static char *kwlist[] = {"content", "flags", NULL};
447+
static char *kwlist[] = {"content", "flags", "default", NULL};
433448
PyObject *content;
449+
PyObject *default_func = NULL;
434450
yyjson_read_err err;
435451
yyjson_read_flag r_flag = 0;
436452

437453
if (!PyArg_ParseTupleAndKeywords(
438-
args, kwds, "O|$I", kwlist, &content, &r_flag
454+
args, kwds, "O|$IO", kwlist, &content, &r_flag, &default_func
439455
)) {
440456
return -1;
441457
}
442458

459+
if (default_func && !PyCallable_Check(default_func)) {
460+
PyErr_SetString(PyExc_TypeError, "default must be callable");
461+
return -1;
462+
}
463+
464+
self->default_func = default_func;
465+
Py_XINCREF(default_func);
466+
443467
if (yyjson_unlikely(pathlib == NULL)) {
444468
pathlib = PyImport_ImportModule("pathlib");
445469
if (yyjson_unlikely(pathlib == NULL)) {
@@ -523,7 +547,7 @@ static int Document_init(DocumentObject *self, PyObject *args, PyObject *kwds) {
523547
return -1;
524548
}
525549

526-
yyjson_mut_val *val = mut_primitive_to_element(self->m_doc, content);
550+
yyjson_mut_val *val = mut_primitive_to_element(self, self->m_doc, content);
527551

528552
if (val == NULL) {
529553
return -1;

yyjson/document.h

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ typedef struct {
1717
yyjson_doc* i_doc;
1818
/** The memory allocator in use for this document. */
1919
yyjson_alc* alc;
20+
/** default callback for serializing unknown types. */
21+
PyObject* default_func;
2022
} DocumentObject;
2123

2224
extern PyTypeObject DocumentType;

0 commit comments

Comments
 (0)