Repository: tefra/xsdata
Pull request #339 (2020-12-03 21:40:12)

JSON Effort

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339&metric=new_duplicated_lines_density&view=list)


codecov[bot]: # [Codecov](https://codecov.io/gh/tefra/xsdata/pull/339?src=pr&el=h1) Report
> Merging [#339](https://codecov.io/gh/tefra/xsdata/pull/339?src=pr&el=desc) (09e94b0) into [master](https://codecov.io/gh/tefra/xsdata/commit/cc9b746b5fc1de4090c46316b734adc0acd94678?el=desc) (cc9b746) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/tefra/xsdata/pull/339/graphs/tree.svg?width=650&height=150&src=pr&token=YzDDLtywvl)](https://codecov.io/gh/tefra/xsdata/pull/339?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master      #339   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           70        70           
  Lines         5676      5680    +4     
  Branches       987       987           
=========================================
+ Hits          5676      5680    +4     
```


| [Impacted Files](https://codecov.io/gh/tefra/xsdata/pull/339?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [xsdata/formats/dataclass/parsers/json.py](https://codecov.io/gh/tefra/xsdata/pull/339/diff?src=pr&el=tree#diff-eHNkYXRhL2Zvcm1hdHMvZGF0YWNsYXNzL3BhcnNlcnMvanNvbi5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/tefra/xsdata/pull/339?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/tefra/xsdata/pull/339?src=pr&el=footer). Last update [cc9b746...09e94b0](https://codecov.io/gh/tefra/xsdata/pull/339?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339&metric=new_duplicated_lines_density&view=list)


sonarcloud[bot]: Kudos, SonarCloud Quality Gate passed!

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/bug.png' alt='Bug' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG) [0 Bugs](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=BUG)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/vulnerability.png' alt='Vulnerability' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY) [0 Vulnerabilities](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=VULNERABILITY)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/security_hotspot.png' alt='Security Hotspot' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT) [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=tefra_xsdata&pullRequest=339&resolved=false&types=SECURITY_HOTSPOT)  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/code_smell.png' alt='Code Smell' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL) [<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/RatingBadge/A.png' alt='A' width='16' height='16' />](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL) [0 Code Smells](https://sonarcloud.io/project/issues?id=tefra_xsdata&pullRequest=339&resolved=false&types=CODE_SMELL)

[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/CoverageChart/NoCoverageInfo.png' alt='No Coverage information' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339) No Coverage information  
[<img src='https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/Duplications/3.png' alt='0.0%' width='16' height='16' />](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339&metric=new_duplicated_lines_density&view=list) [0.0% Duplication](https://sonarcloud.io/component_measures?id=tefra_xsdata&pullRequest=339&metric=new_duplicated_lines_density&view=list)



Patch:
diff --git a/xsdata/formats/dataclass/models/elements.py b/xsdata/formats/dataclass/models/elements.py
index 380cb1d9..1b69552a 100644
--- a/xsdata/formats/dataclass/models/elements.py
+++ b/xsdata/formats/dataclass/models/elements.py
@@ -103,11 +103,6 @@ class XmlVar:
         """Return whether the field is a text element."""
         return False
 
-    @property
-    def local_name(self) -> str:
-        """The element local name."""
-        return split_qname(self.qname)[1]
-
     def matches(self, qname: str) -> bool:
         """
         Match the field qualified local name to the given qname.
diff --git a/xsdata/formats/dataclass/parsers/json.py b/xsdata/formats/dataclass/parsers/json.py
index 133d99ad..96934d50 100644
--- a/xsdata/formats/dataclass/parsers/json.py
+++ b/xsdata/formats/dataclass/parsers/json.py
@@ -18,6 +18,7 @@ from xsdata.formats.dataclass.models.elements import XmlVar
 from xsdata.formats.dataclass.models.generics import AnyElement
 from xsdata.formats.dataclass.models.generics import DerivedElement
 from xsdata.formats.dataclass.parsers.utils import ParserUtils
+from xsdata.utils.constants import EMPTY_MAP
 
 
 @dataclass
@@ -66,22 +67,21 @@ class JsonParser(AbstractParser):
         if var.is_elements:
             return self.bind_choice(value, var)
 
-        return ParserUtils.parse_value(value, var.types, var.default, tokens=var.tokens)
+        return self.parse_value(value, var)
 
     def bind_dataclass(self, data: Dict, clazz: Type[T]) -> T:
-        """
-        Recursively build the given model from the input dict data.
-
-        :raise ParserError: When parsing fails for any reason
-        """
+        """Recursively build the given model from the input dict data."""
         params = {}
         for var in self.context.build(clazz).vars:
-            value = self.get_value(data, var)
+            value = data.get(var.name)
 
             if value is None or not var.init:
                 continue
 
             if var.is_list:
+                if not isinstance(value, list):
+                    raise ParserError(f"Key `{var.name}` value is not iterable")
+
                 params[var.name] = [self.bind_value(var, val) for val in value]
             else:
                 params[var.name] = self.bind_value(var, value)
@@ -89,6 +89,7 @@ class JsonParser(AbstractParser):
         return clazz(**params)  # type: ignore
 
     def bind_dataclass_union(self, value: Dict, var: XmlVar) -> Any:
+        """Bind data to all possible models and return the best candidate."""
         obj = None
         max_score = -1.0
         for clazz in var.types:
@@ -101,11 +102,14 @@ class JsonParser(AbstractParser):
         return obj
 
     def bind_wildcard(self, value: Any) -> Any:
-        return (
-            value if isinstance(value, str) else self.bind_dataclass(value, AnyElement)
-        )
+        """Bind data to a wildcard model."""
+        if isinstance(value, Dict):
+            return self.bind_dataclass(value, AnyElement)
+
+        return value
 
     def bind_choice(self, value: Any, var: XmlVar) -> Any:
+        """Bind data to one of the choice models."""
         if not isinstance(value, dict):
             return self.bind_choice_simple(value, var)
 
@@ -115,6 +119,8 @@ class JsonParser(AbstractParser):
         return self.bind_choice_dataclass(value, var)
 
     def bind_choice_simple(self, value: Any, var: XmlVar) -> Any:
+        """Bind data to one of the simple choice types and return the first
+        that succeeds."""
         orig_type = type(value)
         for choice in var.choices:
             if choice.dataclass:
@@ -130,6 +136,7 @@ class JsonParser(AbstractParser):
         return value
 
     def bind_choice_generic(self, value: Dict, var: XmlVar) -> Any:
+        """Bind data to a either a derived or a user derived model."""
         qname = value["qname"]
         choice = var.find_choice(qname)
 
@@ -144,6 +151,7 @@ class JsonParser(AbstractParser):
         return self.bind_dataclass(value, AnyElement)
 
     def bind_choice_dataclass(self, value: Dict, var: XmlVar) -> Any:
+        """Bind data to the best matching choice model."""
         keys = set(value.keys())
         for choice in var.choices:
             if choice.clazz:
@@ -153,23 +161,12 @@ class JsonParser(AbstractParser):
 
         raise ParserError(f"XmlElements undefined choice: `{var.name}` for `{value}`")
 
-    @staticmethod
-    def get_value(data: Dict, var: XmlVar) -> Any:
-        """Find the var value in the given dictionary or return the default var
-        value."""
-
-        local_name = var.local_name
-        if local_name in data:
-            value = data[local_name]
-        elif var.name in data:
-            value = data[var.name]
-        else:
-            return None
-
-        if var.is_list and not isinstance(value, list):
-            value = [value]
-
-        return value
+    @classmethod
+    def parse_value(cls, value: Any, var: XmlVar) -> Any:
+        """Convert any value to one of the given var types."""
+        return ParserUtils.parse_value(
+            value, var.types, var.default, ns_map=EMPTY_MAP, tokens=var.tokens
+        )
 
 
 @dataclass


Test patch:
diff --git a/tests/formats/dataclass/parsers/test_json.py b/tests/formats/dataclass/parsers/test_json.py
index 63ed15c7..48b2abdb 100644
--- a/tests/formats/dataclass/parsers/test_json.py
+++ b/tests/formats/dataclass/parsers/test_json.py
@@ -66,6 +66,12 @@ class JsonParserTests(TestCase):
             str(cm.exception),
         )
 
+    def test_parser_with_non_iterable_value(self):
+        with self.assertRaises(ParserError) as cm:
+            self.parser.from_string('{"book": 1}')
+
+        self.assertEqual("Key `book` value is not iterable", str(cm.exception))
+
     def test_bind_value_with_attributes_var(self):
         var = XmlAttributes(name="a", qname="a")
         value = {"a": 1}
@@ -162,13 +168,3 @@ class JsonParserTests(TestCase):
             "XmlElements undefined choice: `compound` for qname `foo`",
             str(cm.exception),
         )
-
-    def test_get_value(self):
-        data = dict(foo="bar", bar="foo")
-
-        foo_field = XmlText(name="foo", qname="foo", types=[str])
-        bar_field = XmlElement(name="bar", qname="bar", types=[str], list_element=True)
-
-        self.assertEqual("bar", self.parser.get_value(data, foo_field))
-        self.assertEqual(["foo"], self.parser.get_value(data, bar_field))
-        self.assertIsNone(self.parser.get_value({}, bar_field))