Repository: pydicom/pydicom

Issue:
Add Tag and VR to the bulk data handling in `from_json`
Currently, if you convert back to a Dataset format from a JSON format, you MUST re-hydrate all of the bulk data URI's or you will loose the information.

This causes a problem if you just wish to use the Dataset's header (maybe to extract some data, or rearrange some data), because now you have to pay the cost of getting all the pixel data and then handling the pixel data again upon conversion back to JSON

**Describe the solution you'd like**
Add the tag and the vr to the bulk data handler in `from_json` (this can be done in a backwards compatible way). This will allow the user to store the BulkDataURI's by tag in a map, return dummy data large enough to trigger the bulk handling when to_json is called next, and to use the map to convert back to the original URI's when bulk handling is triggered from to_json.

I'm going to drop a PR tomorrow that does this in a fully backward compatible, non-breaking fashion.



Discussion:

Patch:
diff --git a/pydicom/dataelem.py b/pydicom/dataelem.py
--- a/pydicom/dataelem.py
+++ b/pydicom/dataelem.py
@@ -231,7 +231,12 @@ def from_json(
         vr: str,
         value: object,
         value_key: Union[str, None],
-        bulk_data_uri_handler: Optional[Callable[[str], object]] = None
+        bulk_data_uri_handler: Optional[
+            Union[
+                Callable[[BaseTag, str, str], object],
+                Callable[[str], object]
+            ]
+        ] = None
     ) -> _DataElement:
         """Return a :class:`DataElement` from JSON.
 
@@ -251,7 +256,8 @@ def from_json(
             Key of the data element that contains the value
             (options: ``{"Value", "InlineBinary", "BulkDataURI"}``)
         bulk_data_uri_handler: callable or None
-            Callable function that accepts the "BulkDataURI" of the JSON
+            Callable function that accepts either the tag, vr and "BulkDataURI"
+            or just the "BulkDataURI" of the JSON
             representation of a data element and returns the actual value of
             that data element (retrieved via DICOMweb WADO-RS)
 
@@ -260,8 +266,14 @@ def from_json(
         DataElement
         """
         # TODO: test wado-rs retrieve wrapper
-        converter = JsonDataElementConverter(dataset_class, tag, vr, value,
-                                             value_key, bulk_data_uri_handler)
+        converter = JsonDataElementConverter(
+            dataset_class,
+            tag,
+            vr,
+            value,
+            value_key,
+            bulk_data_uri_handler
+        )
         elem_value = converter.get_element_values()
         try:
             return cls(tag=tag, value=elem_value, VR=vr)
diff --git a/pydicom/dataset.py b/pydicom/dataset.py
--- a/pydicom/dataset.py
+++ b/pydicom/dataset.py
@@ -2218,7 +2218,12 @@ def walk(
     def from_json(
         cls: Type[_Dataset],
         json_dataset: Union[Dict[str, bytes], str],
-        bulk_data_uri_handler: Optional[Callable[[bytes], object]] = None
+        bulk_data_uri_handler: Optional[
+            Union[
+                Callable[[BaseTag, str, str], object],
+                Callable[[str], object]
+            ]
+        ] = None
     ) -> _Dataset:
         """Add elements to the :class:`Dataset` from DICOM JSON format.
 
@@ -2232,7 +2237,8 @@ def from_json(
             :class:`dict` or :class:`str` representing a DICOM Data Set
             formatted based on the DICOM JSON Model.
         bulk_data_uri_handler : callable, optional
-            Callable function that accepts the "BulkDataURI" of the JSON
+            Callable function that accepts either the tag, vr and "BulkDataURI"
+            or just the "BulkDataURI" of the JSON
             representation of a data element and returns the actual value of
             data element (retrieved via DICOMweb WADO-RS).
 
@@ -2263,7 +2269,7 @@ def from_json(
     def to_json_dict(
         self,
         bulk_data_threshold: int = 1024,
-        bulk_data_element_handler: Optional[Callable[[DataElement], bytes]] = None  # noqa
+        bulk_data_element_handler: Optional[Callable[[DataElement], str]] = None  # noqa
     ) -> _Dataset:
         """Return a dictionary representation of the :class:`Dataset`
         conforming to the DICOM JSON Model as described in the DICOM
@@ -2301,7 +2307,7 @@ def to_json_dict(
     def to_json(
         self,
         bulk_data_threshold: int = 1024,
-        bulk_data_element_handler: Optional[Callable[[DataElement], bytes]] = None,  # noqa
+        bulk_data_element_handler: Optional[Callable[[DataElement], str]] = None,  # noqa
         dump_handler: Optional[Callable[["Dataset"], str]] = None
     ) -> str:
         """Return a JSON representation of the :class:`Dataset`.
diff --git a/pydicom/jsonrep.py b/pydicom/jsonrep.py
--- a/pydicom/jsonrep.py
+++ b/pydicom/jsonrep.py
@@ -2,9 +2,12 @@
 """Methods for converting Datasets and DataElements to/from json"""
 
 import base64
+from inspect import signature
+import inspect
+from typing import Callable, Optional, Union
 import warnings
 
-from pydicom.valuerep import PersonName
+from pydicom.tag import BaseTag
 
 # Order of keys is significant!
 JSON_VALUE_KEYS = ('Value', 'BulkDataURI', 'InlineBinary',)
@@ -54,8 +57,20 @@ class JsonDataElementConverter:
     .. versionadded:: 1.4
     """
 
-    def __init__(self, dataset_class, tag, vr, value, value_key,
-                 bulk_data_uri_handler):
+    def __init__(
+        self,
+        dataset_class,
+        tag,
+        vr,
+        value,
+        value_key,
+        bulk_data_uri_handler: Optional[
+            Union[
+                Callable[[BaseTag, str, str], object],
+                Callable[[str], object]
+            ]
+        ] = None
+    ):
         """Create a new converter instance.
 
         Parameters
@@ -63,7 +78,7 @@ def __init__(self, dataset_class, tag, vr, value, value_key,
         dataset_class : dataset.Dataset derived class
             Class used to create sequence items.
         tag : BaseTag
-            The data element tag.
+            The data element tag or int.
         vr : str
             The data element value representation.
         value : list
@@ -72,7 +87,8 @@ def __init__(self, dataset_class, tag, vr, value, value_key,
             Key of the data element that contains the value
             (options: ``{"Value", "InlineBinary", "BulkDataURI"}``)
         bulk_data_uri_handler: callable or None
-            Callable function that accepts the "BulkDataURI" of the JSON
+            Callable function that accepts either the tag, vr and "BulkDataURI"
+            or just the "BulkDataURI" of the JSON
             representation of a data element and returns the actual value of
             that data element (retrieved via DICOMweb WADO-RS)
         """
@@ -81,7 +97,15 @@ def __init__(self, dataset_class, tag, vr, value, value_key,
         self.vr = vr
         self.value = value
         self.value_key = value_key
-        self.bulk_data_uri_handler = bulk_data_uri_handler
+        if (
+            bulk_data_uri_handler and
+            len(signature(bulk_data_uri_handler).parameters) == 1
+        ):
+            def wrapped_bulk_data_handler(tag, vr, value):
+                return bulk_data_uri_handler(value)
+            self.bulk_data_element_handler = wrapped_bulk_data_handler
+        else:
+            self.bulk_data_element_handler = bulk_data_uri_handler
 
     def get_element_values(self):
         """Return a the data element value or list of values.
@@ -124,13 +148,13 @@ def get_element_values(self):
             if not isinstance(value, str):
                 fmt = '"{}" of data element "{}" must be a string.'
                 raise TypeError(fmt.format(self.value_key, self.tag))
-            if self.bulk_data_uri_handler is None:
+            if self.bulk_data_element_handler is None:
                 warnings.warn(
                     'no bulk data URI handler provided for retrieval '
                     'of value of data element "{}"'.format(self.tag)
                 )
                 return empty_value_for_VR(self.vr, raw=True)
-            return self.bulk_data_uri_handler(value)
+            return self.bulk_data_element_handler(self.tag, self.vr, value)
         return empty_value_for_VR(self.vr)
 
     def get_regular_element_value(self, value):


Test patch:
diff --git a/pydicom/tests/test_json.py b/pydicom/tests/test_json.py
--- a/pydicom/tests/test_json.py
+++ b/pydicom/tests/test_json.py
@@ -334,7 +334,18 @@ def test_invalid_bulkdata_uri(self):
             Dataset.from_json(ds_json)
 
     def test_bulk_data_reader_is_called(self):
-        def bulk_data_reader(_):
+        def bulk_data_reader(value):
+            return b'xyzzy'
+
+        json_data = {
+            "00091002": {"vr": "OB", "BulkDataURI": "https://a.dummy.url"}
+        }
+        ds = Dataset().from_json(json.dumps(json_data), bulk_data_reader)
+
+        assert b'xyzzy' == ds[0x00091002].value
+
+    def test_bulk_data_reader_is_called_2(self):
+        def bulk_data_reader(tag, vr, value):
             return b'xyzzy'
 
         json_data = {