Repository: pydicom/pydicom

Issue:
Reading Json with InlineBinary sequences fails
**Describe the bug**
When reading DICOM-json that contains a tag that is known to PyDicom to have a sequence VR, but is encoded as UN with InlineBinary, reading the data fails. 

**Expected behavior**
InlineBinary sequences should be read and expanded as other primitive types are. 

**Steps To Reproduce**

The following code creates a minimal implicit-VR file with a sequence in a private attribute. When read back, the VR of the sequence is UN and the json version is forced to use InlineBinary. Switching the tag to a attribute known to be a sequence causes the reading from json to fail - it appears the constructor for sequence isn't expecting binary.

This example is contrived, but reproduces the same error seen in a much larger valid dataset where the sending side did not know the VR, and the receiving side did. 

```python
from pydicom.dataset import Dataset
from pydicom.sequence import Sequence
from pydicom import dcmread
import json

inputs = {
    "PatientPosition": "HFS",
    "PatientSetupNumber": "1",
    "SetupTechniqueDescription": "",
}
patientSetups = Dataset()
patientSetups.update(inputs)

# Construct the sequence and add to a private group
seq = Sequence((patientSetups,))

ds = Dataset()
block = ds.private_block(0x000B, "ACME 3.1", create=True)
block.add_new(0x01, "SQ", seq)

# Write implicit VR
ds.is_implicit_VR = True
ds.is_little_endian = True
filename = "test.dcm"
ds.save_as(filename)

# read it back so the SQ is UNknown VR.
ds = dcmread(filename, force=True)

js = ds.to_json()
# print(js)
# {"000B0010": {"Value": ["ACME 3.1"], "vr": "LO"}, 
# "000B1001": {"InlineBinary": "/v8A4B4AAAAYAABRBAAAAEhGUyAKMIIBAgAAADEgCjCyAQAAAAA=", 
# "vr": "UN"}}
js_dict = json.loads(js)

# Create a new json from just the custom sequence, but switch the tag to a known one
inline_binary_elem = js_dict["000B1001"]
new_dict = {"300A0180": inline_binary_elem}  # PatientSetupSequence

# Attempt to read the json
# ValueError: Data element '300A0180' could not be loaded from JSON:
dsj = Dataset.from_json(new_dict)
```

Output:
```
Traceback (most recent call last):
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\dataelem.py", line 285, in from_json
    return cls(tag=tag, value=elem_value, VR=vr)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\dataelem.py", line 219, in __init__
    self.value = value  # calls property setter which will convert
    ^^^^^^^^^^
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\dataelem.py", line 450, in value
    self._value = self._convert_value(val)
                  ^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\dataelem.py", line 508, in _convert_value
    return Sequence(val)
           ^^^^^^^^^^^^^
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\sequence.py", line 61, in __init__
    super().__init__(validate_dataset, iterable or [])
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\multival.py", line 70, in __init__
    self._list.append(type_constructor(x))
                      ^^^^^^^^^^^^^^^^^^^
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\sequence.py", line 21, in validate_dataset
    raise TypeError('Sequence contents must be Dataset instances.')
TypeError: Sequence contents must be Dataset instances.

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "C:\ci\projects\test_with_private_dict.py", line 38, in <module>
    dsj = Dataset.from_json(new_dict)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\dataset.py", line 2538, in from_json
    data_element = DataElement.from_json(
                   ^^^^^^^^^^^^^^^^^^^^^^
  File "C:\Users\auser\AppData\Local\Programs\Python\Python312\Lib\site-packages\pydicom\dataelem.py", line 287, in from_json
    raise ValueError(
ValueError: Data element '300A0180' could not be loaded from JSON: b'\xfe\xff\x00\xe0\x1e\x00\x00\x00\x18\x00\x00Q\x04\x00\x00\x00HFS \n0\x82\x01\x02\x00\x00\x001 \n0\xb2\x01\x00\x00\x00\x00'
```

**Your environment**
```bash
$ python -m pydicom.env_info
module       | version
------       | -------
platform     | Windows-11-10.0.22631-SP0
Python       | 3.12.2 (tags/v3.12.2:6abddd9, Feb  6 2024, 21:26:36) [MSC v.1937 64 bit (AMD64)]
pydicom      | 2.4.4
gdcm         | _module not found_
jpeg_ls      | _module not found_
numpy        | _module not found_
PIL          | _module not found_
pylibjpeg    | _module not found_
openjpeg     | _module not found_
libjpeg      | _module not found_
```


Discussion:
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/pydicom/pydicom/pull/2069?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.25%. Comparing base [(`680f0df`)](https://app.codecov.io/gh/pydicom/pydicom/commit/680f0df2aa7bc728b75c4942d4865d4a1b39f6f5?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom) to head [(`0637468`)](https://app.codecov.io/gh/pydicom/pydicom/commit/063746867715906967b37184c71fa49cb974f5ca?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).


<details><summary>Additional details and impacted files</summary>


```diff
@@           Coverage Diff           @@
##             main    #2069   +/-   ##
=======================================
  Coverage   98.25%   98.25%           
=======================================
  Files          76       76           
  Lines       12468    12471    +3     
=======================================
+ Hits        12250    12253    +3     
  Misses        218      218           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/pydicom/pydicom/pull/2069?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=pydicom).

jstark-tpx: Release notes added. Thanks for the quick review.

Patch:
diff --git a/doc/release_notes/v3.0.0.rst b/doc/release_notes/v3.0.0.rst
index 23268c478..5302d61df 100644
--- a/doc/release_notes/v3.0.0.rst
+++ b/doc/release_notes/v3.0.0.rst
@@ -201,7 +201,8 @@ Enhancements
 * Added :meth:`Dataset.pixel_array_options()<pydicom.dataset.Dataset.pixel_array_options>`
   for controlling pixel data decoding when using :attr:`Dataset.pixel_array
   <pydicom.dataset.Dataset.pixel_array>` with the new :mod:`~pydicom.pixels` backend.
-
+* Improve support for reading and resolving inline binary data with `VR=UN` from Json
+  (:issue:`2062`).
 
 Fixes
 -----
diff --git a/src/pydicom/dataelem.py b/src/pydicom/dataelem.py
index 67170a584..5268f4d7e 100644
--- a/src/pydicom/dataelem.py
+++ b/src/pydicom/dataelem.py
@@ -286,6 +286,17 @@ class DataElement:
             dataset_class, tag, vr, value, value_key, bulk_data_uri_handler
         )
         elem_value = converter.get_element_values()
+
+        if (
+            vr == VR_.UN
+            and config.replace_un_with_known_vr
+            and isinstance(elem_value, bytes)
+        ):
+            raw = RawDataElement(
+                Tag(tag), vr, len(elem_value), elem_value, 0, True, True
+            )
+            elem_value = DataElement_from_raw(raw).value
+
         try:
             return cls(tag=tag, value=elem_value, VR=vr)
         except Exception as exc:
@@ -323,13 +334,14 @@ class DataElement:
         if self.VR in (BYTES_VR | AMBIGUOUS_VR) - {VR_.US_SS}:
             if not self.is_empty:
                 binary_value = self.value
-                encoded_value = base64.b64encode(binary_value).decode("utf-8")
-                if (
-                    bulk_data_element_handler is not None
-                    and len(encoded_value) > bulk_data_threshold
+                # Base64 makes the encoded value 1/3 longer.
+                if bulk_data_element_handler is not None and len(binary_value) > (
+                    (bulk_data_threshold // 4) * 3
                 ):
                     json_element["BulkDataURI"] = bulk_data_element_handler(self)
                 else:
+                    # Json is exempt from padding to even length, see DICOM-CP1920
+                    encoded_value = base64.b64encode(binary_value).decode("utf-8")
                     logger.info(f"encode bulk data element '{self.name}' inline")
                     json_element["InlineBinary"] = encoded_value
         elif self.VR == VR_.SQ:


Test patch:
diff --git a/tests/test_json.py b/tests/test_json.py
index 1564f4593..e7a885533 100644
--- a/tests/test_json.py
+++ b/tests/test_json.py
@@ -340,6 +340,45 @@ class TestBinary:
         ds1 = Dataset.from_json(ds_json)
         assert ds == ds1
 
+    def test_inline_binary_un_sq(self):
+        # Test unknown VR with inline binary and a sequence
+        seq_item = Dataset()
+        seq_item.update(
+            {
+                "PatientPosition": "HFS",
+                "PatientSetupNumber": "1",
+                "SetupTechniqueDescription": "",
+            }
+        )
+        ds_ref = Dataset()
+        ds_ref.PatientSetupSequence = [seq_item]
+
+        ds_json = {
+            "300A0180": {
+                "vr": "UN",
+                "InlineBinary": "/v8A4B4AAAAYAABRBAAAAEhGUyAKMIIBAgAAADEgCjCyAQAAAAA=",
+            }
+        }
+        ds1 = Dataset.from_json(ds_json)
+        assert "PatientSetupSequence" in ds1
+        assert ds1 == ds_ref
+
+    def test_inline_binary_un_pad(self):
+        # Test unknown VR with inline binary and odd-length values
+        # The value ("HFS") is an odd length.
+        # The value is padded with a space before base64 encoding because
+        # UN uses implicit VR little endian.
+        # When reading back, the padding should be removed.
+        ds_json = {
+            "00185100": {
+                "vr": "UN",
+                "InlineBinary": "SEZTIA==",
+            }
+        }
+        ds1 = Dataset.from_json(ds_json)
+        assert "PatientPosition" in ds1
+        assert ds1.PatientPosition == "HFS"
+
     def test_invalid_inline_binary(self):
         msg = (
             "Invalid attribute value for data element '00091002' - the value "