Repository: spdx/tools-python

Issue:
JSON parser: homepage of package not parsed correctly
The `homepage` field of a package could be `"NOASSERTION"` or `"NONE"`.
When parsing a file like the one below
```
{
    "spdxVersion": "SPDX-2.3",
    "documentNamespace": "https://true",
    "creationInfo": {
        "creators": [
            "Person: Mee (mee)"
        ],
        "created": "2023-04-17T12:34:39Z",
        "licenseListVersion": "3.20"
    },
    "dataLicense": "CC0-1.0",
    "SPDXID": "SPDXRef-DOCUMENT",
    "name": "Test 2",
    "packages": [
        {
            "SPDXID": "SPDXRef-package",
            "name": "Test",
            "downloadLocation": "NOASSERTION",
            "homepage": "NOASSERTION"
        }]

}
``` 
a `ValidationError` is raised. This originates from the JSON parser which doesn't process the field correctly.

Discussion:

Patch:
diff --git a/src/spdx/parser/jsonlikedict/file_parser.py b/src/spdx/parser/jsonlikedict/file_parser.py
index eb59fcc..3f4b565 100644
--- a/src/spdx/parser/jsonlikedict/file_parser.py
+++ b/src/spdx/parser/jsonlikedict/file_parser.py
@@ -10,7 +10,10 @@ from spdx.model.file import File, FileType
 from spdx.model.spdx_no_assertion import SpdxNoAssertion
 from spdx.model.spdx_none import SpdxNone
 from spdx.parser.jsonlikedict.checksum_parser import ChecksumParser
-from spdx.parser.jsonlikedict.dict_parsing_functions import parse_field_or_log_error
+from spdx.parser.jsonlikedict.dict_parsing_functions import (
+    parse_field_or_log_error,
+    parse_field_or_no_assertion_or_none,
+)
 from spdx.parser.jsonlikedict.license_expression_parser import LicenseExpressionParser
 from spdx.parser.logger import Logger
 from spdx.parser.parsing_functions import construct_or_raise_parsing_error, raise_parsing_error_if_logger_has_messages
@@ -37,7 +40,9 @@ class FileParser:
 
         attribution_texts: List[str] = file_dict.get("attributionTexts", [])
         comment: Optional[str] = file_dict.get("comment")
-        copyright_text: Optional[str] = file_dict.get("copyrightText")
+        copyright_text: Optional[Union[str, SpdxNoAssertion, SpdxNone]] = parse_field_or_no_assertion_or_none(
+            file_dict.get("copyrightText")
+        )
         file_contributors: List[str] = file_dict.get("fileContributors", [])
         file_types: List[FileType] = parse_field_or_log_error(
             logger, file_dict.get("fileTypes"), self.parse_file_types
diff --git a/src/spdx/parser/jsonlikedict/package_parser.py b/src/spdx/parser/jsonlikedict/package_parser.py
index c79fd11..090a7ad 100644
--- a/src/spdx/parser/jsonlikedict/package_parser.py
+++ b/src/spdx/parser/jsonlikedict/package_parser.py
@@ -58,7 +58,9 @@ class PackageParser:
             logger, package_dict.get("checksums"), self.checksum_parser.parse_checksum, field_is_list=True
         )
         comment: Optional[str] = package_dict.get("comment")
-        copyright_text: Optional[str] = package_dict.get("copyrightText")
+        copyright_text: Optional[Union[str, SpdxNoAssertion, SpdxNone]] = parse_field_or_no_assertion_or_none(
+            package_dict.get("copyrightText")
+        )
         description: Optional[str] = package_dict.get("description")
         download_location: Optional[Union[str, SpdxNoAssertion, SpdxNone]] = parse_field_or_no_assertion_or_none(
             package_dict.get("downloadLocation")
@@ -78,7 +80,9 @@ class PackageParser:
             elif files_analyzed.lower() == "false":
                 files_analyzed = False
 
-        homepage: Optional[str] = package_dict.get("homepage")
+        homepage: Optional[Union[str, SpdxNoAssertion, SpdxNone]] = parse_field_or_no_assertion_or_none(
+            package_dict.get("homepage")
+        )
         license_comments: Optional[str] = package_dict.get("licenseComments")
         license_concluded = parse_field_or_log_error(
             logger, package_dict.get("licenseConcluded"), self.license_expression_parser.parse_license_expression
diff --git a/src/spdx/parser/jsonlikedict/snippet_parser.py b/src/spdx/parser/jsonlikedict/snippet_parser.py
index 52d2194..c4f5250 100644
--- a/src/spdx/parser/jsonlikedict/snippet_parser.py
+++ b/src/spdx/parser/jsonlikedict/snippet_parser.py
@@ -10,7 +10,10 @@ from spdx.model.snippet import Snippet
 from spdx.model.spdx_no_assertion import SpdxNoAssertion
 from spdx.model.spdx_none import SpdxNone
 from spdx.parser.error import SPDXParsingError
-from spdx.parser.jsonlikedict.dict_parsing_functions import parse_field_or_log_error
+from spdx.parser.jsonlikedict.dict_parsing_functions import (
+    parse_field_or_log_error,
+    parse_field_or_no_assertion_or_none,
+)
 from spdx.parser.jsonlikedict.license_expression_parser import LicenseExpressionParser
 from spdx.parser.logger import Logger
 from spdx.parser.parsing_functions import construct_or_raise_parsing_error
@@ -43,7 +46,9 @@ class SnippetParser:
 
         attribution_texts: List[str] = snippet_dict.get("attributionTexts", [])
         comment: Optional[str] = snippet_dict.get("comment")
-        copyright_text: Optional[str] = snippet_dict.get("copyrightText")
+        copyright_text: Optional[Union[str, SpdxNoAssertion, SpdxNone]] = parse_field_or_no_assertion_or_none(
+            snippet_dict.get("copyrightText")
+        )
         license_comment: Optional[str] = snippet_dict.get("licenseComments")
         license_concluded: Optional[Union[LicenseExpression, SpdxNoAssertion, SpdxNone]] = parse_field_or_log_error(
             logger, snippet_dict.get("licenseConcluded"), self.license_expression_parser.parse_license_expression


Test patch:
diff --git a/tests/spdx/parser/jsonlikedict/test_file_parser.py b/tests/spdx/parser/jsonlikedict/test_file_parser.py
index e99c039..6aacf44 100644
--- a/tests/spdx/parser/jsonlikedict/test_file_parser.py
+++ b/tests/spdx/parser/jsonlikedict/test_file_parser.py
@@ -9,12 +9,21 @@ from license_expression import Licensing
 from spdx.model.checksum import Checksum, ChecksumAlgorithm
 from spdx.model.file import FileType
 from spdx.model.spdx_no_assertion import SpdxNoAssertion
+from spdx.model.spdx_none import SpdxNone
 from spdx.parser.error import SPDXParsingError
 from spdx.parser.jsonlikedict.dict_parsing_functions import parse_list_of_elements
 from spdx.parser.jsonlikedict.file_parser import FileParser
 
 
-def test_parse_file():
+@pytest.mark.parametrize(
+    "copyright_text, expected_copyright_text",
+    [
+        ("Copyright 2008-2010 John Smith", "Copyright 2008-2010 John Smith"),
+        ("NOASSERTION", SpdxNoAssertion()),
+        ("NONE", SpdxNone()),
+    ],
+)
+def test_parse_file(copyright_text, expected_copyright_text):
     file_parser = FileParser()
     file_dict = {
         "SPDXID": "SPDXRef-File",
@@ -25,7 +34,7 @@ def test_parse_file():
         ],
         "comment": "The concluded license was taken from the package level that the file was included in.\nThis "
         "information was found in the COPYING.txt file in the xyz directory.",
-        "copyrightText": "Copyright 2008-2010 John Smith",
+        "copyrightText": copyright_text,
         "fileContributors": [
             "The Regents of the University of California",
             "Modified by Paul Mundt lethal@linux-sh.org",
@@ -66,7 +75,7 @@ def test_parse_file():
         == "The concluded license was taken from the package level that the file was included in.\nThis information "
         "was found in the COPYING.txt file in the xyz directory."
     )
-    assert file.copyright_text == "Copyright 2008-2010 John Smith"
+    assert file.copyright_text == expected_copyright_text
     assert file.file_types == [FileType.SOURCE]
     TestCase().assertCountEqual(
         file.contributors,
diff --git a/tests/spdx/parser/jsonlikedict/test_package_parser.py b/tests/spdx/parser/jsonlikedict/test_package_parser.py
index 00cecbd..bb0fea4 100644
--- a/tests/spdx/parser/jsonlikedict/test_package_parser.py
+++ b/tests/spdx/parser/jsonlikedict/test_package_parser.py
@@ -11,12 +11,66 @@ from spdx.model.actor import Actor, ActorType
 from spdx.model.checksum import Checksum, ChecksumAlgorithm
 from spdx.model.package import ExternalPackageRef, ExternalPackageRefCategory, PackagePurpose, PackageVerificationCode
 from spdx.model.spdx_no_assertion import SpdxNoAssertion
+from spdx.model.spdx_none import SpdxNone
 from spdx.parser.error import SPDXParsingError
 from spdx.parser.jsonlikedict.dict_parsing_functions import parse_list_of_elements
 from spdx.parser.jsonlikedict.package_parser import PackageParser
 
 
-def test_parse_package():
+@pytest.mark.parametrize(
+    "homepage, expected_homepage, download_location, expected_download_location, "
+    "copyright_text, expected_copyright_text, originator, expected_originator, supplier, expected_supplier",
+    [
+        (
+            "http://ftp.gnu.org/gnu/glibc",
+            "http://ftp.gnu.org/gnu/glibc",
+            "NOASSERTION",
+            SpdxNoAssertion(),
+            "NONE",
+            SpdxNone(),
+            "Organization: ExampleCodeInspect (contact@example.com)",
+            Actor(ActorType.ORGANIZATION, "ExampleCodeInspect", "contact@example.com"),
+            "NOASSERTION",
+            SpdxNoAssertion(),
+        ),
+        (
+            "NOASSERTION",
+            SpdxNoAssertion(),
+            "NONE",
+            SpdxNone(),
+            "Copyright 2008-2010 John Smith",
+            "Copyright 2008-2010 John Smith",
+            None,
+            None,
+            None,
+            None,
+        ),
+        (
+            "NONE",
+            SpdxNone(),
+            "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+            "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+            "NOASSERTION",
+            SpdxNoAssertion(),
+            "NOASSERTION",
+            SpdxNoAssertion(),
+            "Person: Jane Doe (jane.doe@example.com)",
+            Actor(ActorType.PERSON, "Jane Doe", "jane.doe@example.com"),
+        ),
+    ],
+)
+def test_parse_package(
+    homepage,
+    expected_homepage,
+    download_location,
+    expected_download_location,
+    copyright_text,
+    expected_copyright_text,
+    originator,
+    expected_originator,
+    supplier,
+    expected_supplier,
+):
     package_parser = PackageParser()
 
     package_dict = {
@@ -42,11 +96,11 @@ def test_parse_package():
             },
         ],
         "comment": "This is a comment.",
-        "copyrightText": "Copyright 2008-2010 John Smith",
+        "copyrightText": copyright_text,
         "description": "The GNU C Library defines functions that are specified by the ISO C standard, as well as "
         "additional features specific to POSIX and other derivatives of the Unix operating system, and "
         "extensions specific to GNU systems.",
-        "downloadLocation": "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz",
+        "downloadLocation": download_location,
         "externalRefs": [
             {
                 "referenceCategory": "SECURITY",
@@ -62,14 +116,14 @@ def test_parse_package():
             },
         ],
         "filesAnalyzed": True,
-        "homepage": "http://ftp.gnu.org/gnu/glibc",
+        "homepage": homepage,
         "licenseComments": "The license for this project changed with the release of version x.y.  The version of the "
         "project included here post-dates the license change.",
         "licenseConcluded": "(LGPL-2.0-only OR LicenseRef-3)",
         "licenseDeclared": "(LGPL-2.0-only AND LicenseRef-3)",
         "licenseInfoFromFiles": ["GPL-2.0-only", "LicenseRef-2", "LicenseRef-1", "NOASSERTION"],
         "name": "glibc",
-        "originator": "Organization: ExampleCodeInspect (contact@example.com)",
+        "originator": originator,
         "packageFileName": "glibc-2.11.1.tar.gz",
         "packageVerificationCode": {
             "packageVerificationCodeExcludedFiles": ["./package.spdx"],
@@ -79,7 +133,7 @@ def test_parse_package():
         "releaseDate": "2012-01-29T18:30:22Z",
         "sourceInfo": "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git.",
         "summary": "GNU C library.",
-        "supplier": "Person: Jane Doe (jane.doe@example.com)",
+        "supplier": supplier,
         "validUntilDate": "2014-01-29T18:30:22Z",
         "versionInfo": "2.11.1",
     }
@@ -88,11 +142,11 @@ def test_parse_package():
 
     assert package.spdx_id == "SPDXRef-Package"
     assert package.name == "glibc"
-    assert package.download_location == "http://ftp.gnu.org/gnu/glibc/glibc-ports-2.15.tar.gz"
+    assert package.download_location == expected_download_location
     assert package.version == "2.11.1"
     assert package.file_name == "glibc-2.11.1.tar.gz"
-    assert package.supplier == Actor(ActorType.PERSON, "Jane Doe", "jane.doe@example.com")
-    assert package.originator == Actor(ActorType.ORGANIZATION, "ExampleCodeInspect", "contact@example.com")
+    assert package.supplier == expected_supplier
+    assert package.originator == expected_originator
     assert package.files_analyzed is True
     assert package.verification_code == PackageVerificationCode(
         value="d6a770ba38583ed4bb4525bd96e50461655d2758", excluded_files=["./package.spdx"]
@@ -110,7 +164,7 @@ def test_parse_package():
             ),
         ],
     )
-    assert package.homepage == "http://ftp.gnu.org/gnu/glibc"
+    assert package.homepage == expected_homepage
     assert package.source_info == "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git."
     assert package.license_concluded == Licensing().parse("(LGPL-2.0-only OR LicenseRef-3)")
     TestCase().assertCountEqual(
@@ -128,7 +182,7 @@ def test_parse_package():
         == "The license for this project changed with the release of version x.y.  The version of the project included"
         " here post-dates the license change."
     )
-    assert package.copyright_text == "Copyright 2008-2010 John Smith"
+    assert package.copyright_text == expected_copyright_text
     assert package.summary == "GNU C library."
     assert (
         package.description
diff --git a/tests/spdx/parser/jsonlikedict/test_snippet_parser.py b/tests/spdx/parser/jsonlikedict/test_snippet_parser.py
index 10b0fb2..1cbceb9 100644
--- a/tests/spdx/parser/jsonlikedict/test_snippet_parser.py
+++ b/tests/spdx/parser/jsonlikedict/test_snippet_parser.py
@@ -7,11 +7,20 @@ import pytest
 from license_expression import Licensing
 
 from spdx.model.spdx_no_assertion import SpdxNoAssertion
+from spdx.model.spdx_none import SpdxNone
 from spdx.parser.error import SPDXParsingError
 from spdx.parser.jsonlikedict.snippet_parser import SnippetParser
 
 
-def test_parse_snippet():
+@pytest.mark.parametrize(
+    "copyright_text, expected_copyright_text",
+    [
+        ("Copyright 2008-2010 John Smith", "Copyright 2008-2010 John Smith"),
+        ("NOASSERTION", SpdxNoAssertion()),
+        ("NONE", SpdxNone()),
+    ],
+)
+def test_parse_snippet(copyright_text, expected_copyright_text):
     snippet_parser = SnippetParser()
 
     snippet_dict = {
@@ -19,7 +28,7 @@ def test_parse_snippet():
         "comment": "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a "
         "commercial scanner identified it as being derived from file foo.c in package xyz which is licensed"
         " under GPL-2.0.",
-        "copyrightText": "Copyright 2008-2010 John Smith",
+        "copyrightText": copyright_text,
         "licenseComments": "The concluded license was taken from package xyz, from which the snippet was copied into "
         "the current file. The concluded license information was found in the COPYING.txt file in "
         "package xyz.",
@@ -48,7 +57,7 @@ def test_parse_snippet():
         == "This snippet was identified as significant and highlighted in this Apache-2.0 file, when a commercial "
         "scanner identified it as being derived from file foo.c in package xyz which is licensed under GPL-2.0."
     )
-    assert snippet.copyright_text == "Copyright 2008-2010 John Smith"
+    assert snippet.copyright_text == expected_copyright_text
     assert (
         snippet.license_comment
         == "The concluded license was taken from package xyz, from which the snippet was copied into the current file."