Repository: spdx/tools-python

Issue:
Refactor JSON parser
We want a refactored parser using the new data model (available on https://github.com/spdx/tools-python/tree/refactor-python-tools).

Possible basic interface (maybe extract an abstract base class later) as follows. The parser class should use composition instead of inheritance.
```
class JsonParser:
    creation_info_parser: CreationInfoParser
    package_parser: PackageParser
    file_parser: FileParser
    <...>

    def parse(self, filename: str) -> Document:
        logger = Logger()
         with open(filename) as file:
             input_doc_as_dict = json.load(file)
         document: Document = creation_info_parser.parse(input_doc_as_dict, logger)
         document.packages = package_parser.parse(input_doc_as_dict.get("packages"), logger)
        <...>
        
        if logger.has_errors():
            raise SpdxParsingError(logger.get_errors())
        return document
```

Discussion:

Patch:
diff --git a/src/model/typing/constructor_type_errors.py b/src/model/typing/constructor_type_errors.py
index cd02944..52b3a4d 100644
--- a/src/model/typing/constructor_type_errors.py
+++ b/src/model/typing/constructor_type_errors.py
@@ -10,3 +10,6 @@ class ConstructorTypeErrors(TypeError):
 
     def __init__(self, messages: List[str]):
         self.messages = messages
+
+    def get_messages(self):
+        return list(self.messages)
diff --git a/src/parser/__init__.py b/src/parser/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/parser/error.py b/src/parser/error.py
new file mode 100644
index 0000000..580c4cf
--- /dev/null
+++ b/src/parser/error.py
@@ -0,0 +1,21 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import List
+
+
+class SPDXParsingError(Exception):
+    messages: List[str]
+
+    def __init__(self, messages: List[str]):
+        self.messages = messages
+
+    def get_messages(self):
+        return list(self.messages)
diff --git a/src/parser/json/__init__.py b/src/parser/json/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/src/parser/json/actor_parser.py b/src/parser/json/actor_parser.py
new file mode 100644
index 0000000..cfd835d
--- /dev/null
+++ b/src/parser/json/actor_parser.py
@@ -0,0 +1,56 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import re
+from typing import Union, Pattern, Match, Optional
+
+from src.model.actor import Actor, ActorType
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import construct_or_raise_parsing_error
+
+
+class ActorParser:
+
+    @staticmethod
+    def parse_actor(actor: str) -> Actor:
+        tool_re: Pattern = re.compile(r"^Tool:\s*(.+)", re.UNICODE)
+        person_re: Pattern = re.compile(r"^Person:\s*(([^(])+)(\((.*)\))?", re.UNICODE)
+        org_re: Pattern = re.compile(r"^Organization:\s*(([^(])+)(\((.*)\))?", re.UNICODE)
+        tool_match: Match = tool_re.match(actor)
+        person_match: Match = person_re.match(actor)
+        org_match: Match = org_re.match(actor)
+
+        if tool_match:
+            name: str = tool_match.group(1).strip()
+            creator = construct_or_raise_parsing_error(Actor, dict(actor_type=ActorType.TOOL, name=name))
+
+        elif person_match:
+            name: str = person_match.group(1).strip()
+            email: Optional[str] = ActorParser.get_email_or_none(person_match)
+            creator = construct_or_raise_parsing_error(Actor, dict(actor_type=ActorType.PERSON, name=name, email=email))
+        elif org_match:
+            name: str = org_match.group(1).strip()
+            email: Optional[str] = ActorParser.get_email_or_none(org_match)
+            creator = construct_or_raise_parsing_error(Actor,
+                                                       dict(actor_type=ActorType.ORGANIZATION, name=name, email=email))
+        else:
+            raise SPDXParsingError([f"Actor {actor} doesn't match any of person, organization or tool."])
+
+        return creator
+
+    @staticmethod
+    def get_email_or_none(match: Match) -> Optional[str]:
+        email_match = match.group(4)
+        if email_match and email_match.strip():
+            email = email_match.strip()
+        else:
+            email = None
+        return email
diff --git a/src/parser/json/annotation_parser.py b/src/parser/json/annotation_parser.py
new file mode 100644
index 0000000..0dbf4e7
--- /dev/null
+++ b/src/parser/json/annotation_parser.py
@@ -0,0 +1,104 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from datetime import datetime
+from typing import Dict, Optional, List
+
+from src.model.actor import Actor
+from src.model.annotation import Annotation, AnnotationType
+from src.parser.error import SPDXParsingError
+from src.parser.json.actor_parser import ActorParser
+from src.parser.json.dict_parsing_functions import datetime_from_str, construct_or_raise_parsing_error, \
+    parse_field_or_log_error, append_parsed_field_or_log_error, raise_parsing_error_if_logger_has_messages, \
+    parse_list_of_elements
+from src.parser.logger import Logger
+
+
+class AnnotationParser:
+    logger: Logger
+    actor_parser: ActorParser
+
+    def __init__(self):
+        self.logger = Logger()
+        self.actor_parser = ActorParser()
+
+    def parse_all_annotations(self, input_doc_dict: Dict) -> List[Annotation]:
+        annotations = []
+        self.parse_annotations_from_object(annotations, [input_doc_dict])
+        reviews: List[Dict] = input_doc_dict.get("revieweds", [])
+        for review in reviews:
+            annotations = append_parsed_field_or_log_error(
+                self.logger, annotations, review, lambda x: self.parse_review(x, spdx_id=input_doc_dict.get("SPDXID")))
+        packages: List[Dict] = input_doc_dict.get("packages", [])
+        self.parse_annotations_from_object(annotations, packages)
+        files: List[Dict] = input_doc_dict.get("files", [])
+        self.parse_annotations_from_object(annotations, files)
+        snippets: List[Dict] = input_doc_dict.get("snippets", [])
+        self.parse_annotations_from_object(annotations, snippets)
+
+        raise_parsing_error_if_logger_has_messages(self.logger, "annotations")
+        return annotations
+
+    def parse_annotations_from_object(self, annotations: List[Annotation], element_list: List[Dict]):
+        for element in element_list:
+            element_spdx_id: Optional[str] = element.get("SPDXID")
+            element_annotations: List[Dict] = element.get("annotations", [])
+            annotations.extend(parse_field_or_log_error(self.logger, element_annotations,
+
+                                                            lambda y: self.parse_annotation(y, spdx_id=element_spdx_id),
+                                                            [], True))
+
+    def parse_annotation(self, annotation_dict: Dict, spdx_id: Optional[str] = None) -> Annotation:
+        logger = Logger()
+        spdx_id: Optional[str] = annotation_dict.get("SPDXID") or spdx_id
+
+        annotation_type: Optional[AnnotationType] = parse_field_or_log_error(logger,
+                                                                             annotation_dict.get("annotationType"),
+                                                                             self.parse_annotation_type)
+
+        annotator: Optional[Actor] = parse_field_or_log_error(logger, annotation_dict.get("annotator"),
+                                                              self.actor_parser.parse_actor)
+
+        annotation_date: Optional[datetime] = parse_field_or_log_error(logger, annotation_dict.get("annotationDate"),
+                                                                       datetime_from_str)
+
+        annotation_comment: Optional[str] = annotation_dict.get("comment")
+        raise_parsing_error_if_logger_has_messages(logger, "Annotation")
+        annotation_dict = construct_or_raise_parsing_error(Annotation,
+                                                           dict(spdx_id=spdx_id, annotation_type=annotation_type,
+                                                                annotator=annotator, annotation_date=annotation_date,
+                                                                annotation_comment=annotation_comment))
+
+        return annotation_dict
+
+    @staticmethod
+    def parse_annotation_type(annotation_type: str) -> AnnotationType:
+        try:
+            return AnnotationType[annotation_type]
+        except KeyError:
+            raise SPDXParsingError([f"Invalid AnnotationType: {annotation_type}"])
+
+    def parse_review(self, review_dict: Dict, spdx_id: str) -> Annotation:
+        logger = Logger()
+        annotator: Optional[Actor] = parse_field_or_log_error(logger, review_dict.get("reviewer"),
+                                                              self.actor_parser.parse_actor)
+
+        annotation_date: Optional[datetime] = parse_field_or_log_error(logger, review_dict.get("reviewDate"),
+                                                                       datetime_from_str)
+
+        annotation_type = AnnotationType.REVIEW
+        comment: Optional[str] = review_dict.get("comment")
+        raise_parsing_error_if_logger_has_messages(logger, "Annotation from revieweds")
+
+        annotation = construct_or_raise_parsing_error(Annotation,
+                                                      dict(spdx_id=spdx_id, annotation_type=annotation_type,
+                                                           annotator=annotator, annotation_date=annotation_date,
+                                                           annotation_comment=comment))
+        return annotation
diff --git a/src/parser/json/checksum_parser.py b/src/parser/json/checksum_parser.py
new file mode 100644
index 0000000..c86cb3e
--- /dev/null
+++ b/src/parser/json/checksum_parser.py
@@ -0,0 +1,38 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Dict, List, Optional
+
+from src.model.checksum import Checksum, ChecksumAlgorithm
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import append_parsed_field_or_log_error, \
+    raise_parsing_error_if_logger_has_messages, json_str_to_enum_name, construct_or_raise_parsing_error
+from src.parser.logger import Logger
+
+
+class ChecksumParser:
+    logger: Logger
+
+    def __init__(self):
+        self.logger = Logger()
+
+    @staticmethod
+    def parse_checksum(checksum_dict: Dict) -> Checksum:
+        logger = Logger()
+        algorithm: str = json_str_to_enum_name(checksum_dict.get("algorithm", ""))
+        try:
+            checksum_algorithm = ChecksumAlgorithm[algorithm]
+        except KeyError:
+            logger.append(f"Invalid ChecksumAlgorithm: {algorithm}")
+            checksum_algorithm = None
+        checksum_value: Optional[str] = checksum_dict.get("checksumValue")
+        raise_parsing_error_if_logger_has_messages(logger, "Checksum")
+        checksum = construct_or_raise_parsing_error(Checksum, dict(algorithm=checksum_algorithm, value=checksum_value))
+        return checksum
diff --git a/src/parser/json/creation_info_parser.py b/src/parser/json/creation_info_parser.py
new file mode 100644
index 0000000..5f8eaab
--- /dev/null
+++ b/src/parser/json/creation_info_parser.py
@@ -0,0 +1,122 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from datetime import datetime
+from typing import Dict, Optional, List
+
+from src.model.actor import Actor
+from src.model.checksum import Checksum
+from src.model.document import CreationInfo
+from src.model.external_document_ref import ExternalDocumentRef
+from src.model.version import Version
+from src.parser.error import SPDXParsingError
+from src.parser.json.actor_parser import ActorParser
+from src.parser.json.checksum_parser import ChecksumParser
+from src.parser.json.dict_parsing_functions import append_parsed_field_or_log_error, datetime_from_str, \
+    raise_parsing_error_if_logger_has_messages, construct_or_raise_parsing_error, parse_field_or_log_error, \
+    parse_field_or_no_assertion
+from src.parser.logger import Logger
+
+
+class CreationInfoParser:
+    logger: Logger
+    actor_parser: ActorParser
+    checksum_parser: ChecksumParser
+
+    def __init__(self):
+        self.logger = Logger()
+        self.actor_parser = ActorParser()
+        self.checksum_parser = ChecksumParser()
+
+    def parse_creation_info(self, doc_dict: Dict) -> CreationInfo:
+        logger = Logger()
+        spdx_version: Optional[str] = doc_dict.get("spdxVersion")
+        spdx_id: Optional[str] = doc_dict.get("SPDXID")
+        name: Optional[str] = doc_dict.get("name")
+        document_namespace: Optional[str] = doc_dict.get("documentNamespace")
+        creation_info_dict: Optional[Dict] = doc_dict.get("creationInfo")
+
+        # There are nested required properties. If creationInfo is not set, we cannot continue parsing.
+        if creation_info_dict is None:
+            logger.append("CreationInfo does not exist.")
+            raise SPDXParsingError([f"Error while parsing document {name}: {logger.get_messages()}"])
+
+        creators: List[Actor] = parse_field_or_log_error(logger, creation_info_dict.get("creators"),
+                                                         self.parse_creators)
+
+        created: Optional[datetime] = parse_field_or_log_error(logger, creation_info_dict.get("created"),
+                                                               datetime_from_str)
+
+        creator_comment: Optional[str] = creation_info_dict.get("comment")
+        data_license: Optional[str] = doc_dict.get("dataLicense")
+
+        external_document_refs: List[ExternalDocumentRef] = parse_field_or_log_error(logger, doc_dict.get(
+            "externalDocumentRefs"), self.parse_external_document_refs)
+        license_list_version: Optional[Version] = parse_field_or_log_error(logger,
+                                                                           creation_info_dict.get("licenseListVersion"),
+                                                                           self.parse_version)
+        document_comment: Optional[str] = doc_dict.get("comment")
+        raise_parsing_error_if_logger_has_messages(logger, "Document")
+
+        creation_info = construct_or_raise_parsing_error(CreationInfo,
+                                                         dict(spdx_version=spdx_version, spdx_id=spdx_id, name=name,
+                                                              document_namespace=document_namespace,
+                                                              creators=creators, created=created,
+                                                              license_list_version=license_list_version,
+                                                              document_comment=document_comment,
+                                                              creator_comment=creator_comment,
+                                                              data_license=data_license,
+                                                              external_document_refs=external_document_refs))
+
+        return creation_info
+
+    def parse_creators(self, creators_list_from_dict: List[str]) -> List[Actor]:
+        logger = Logger()
+        creators = []
+        for creator_str in creators_list_from_dict:
+            creators = append_parsed_field_or_log_error(logger, creators, creator_str, lambda x: parse_field_or_no_assertion(x, self.actor_parser.parse_actor))
+
+        raise_parsing_error_if_logger_has_messages(logger)
+        return creators
+
+    @staticmethod
+    def parse_version(version_str: str) -> Version:
+        try:
+            return Version.from_string(version_str)
+        except ValueError as err:
+            raise SPDXParsingError([f"Error while parsing version {version_str}: {err.args[0]}"])
+
+    def parse_external_document_refs(self, external_document_ref_dicts: List[Dict]) -> List[ExternalDocumentRef]:
+        logger = Logger()
+        external_document_refs = []
+        for external_document_ref_dict in external_document_ref_dicts:
+            external_document_ref: ExternalDocumentRef = parse_field_or_log_error(logger, external_document_ref_dict,
+                                                                                  self.parse_external_document_ref)
+
+            external_document_refs.append(external_document_ref)
+
+        raise_parsing_error_if_logger_has_messages(logger)
+        return external_document_refs
+
+    def parse_external_document_ref(self, external_document_ref_dict: Dict) -> ExternalDocumentRef:
+        logger = Logger()
+        checksum: Optional[Checksum] = parse_field_or_log_error(logger, external_document_ref_dict.get("checksum"),
+                                                                self.checksum_parser.parse_checksum)
+
+        external_document_id: Optional[str] = external_document_ref_dict.get("externalDocumentId")
+        document_uri: Optional[str] = external_document_ref_dict.get("spdxDocument")
+        raise_parsing_error_if_logger_has_messages(logger, "ExternalDocumentRef")
+        external_document_ref: ExternalDocumentRef = construct_or_raise_parsing_error(ExternalDocumentRef,
+                                                                                 dict(
+                                                                                     document_ref_id=external_document_id,
+                                                                                     checksum=checksum,
+                                                                                     document_uri=document_uri))
+
+        return external_document_ref
diff --git a/src/parser/json/dict_parsing_functions.py b/src/parser/json/dict_parsing_functions.py
new file mode 100644
index 0000000..55262b6
--- /dev/null
+++ b/src/parser/json/dict_parsing_functions.py
@@ -0,0 +1,102 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from datetime import datetime
+from typing import Any, Callable, Dict, List, Union, Optional
+
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
+from src.model.typing.constructor_type_errors import ConstructorTypeErrors
+from src.parser.error import SPDXParsingError
+from src.parser.logger import Logger
+
+
+def datetime_from_str(date_str: str) -> datetime:
+    if not isinstance(date_str, str):
+        raise SPDXParsingError([f"Could not convert str to datetime, invalid type: {type(date_str).__name__}"])
+    try:
+        date = datetime.strptime(date_str, "%Y-%m-%dT%H:%M:%SZ")
+    except ValueError:
+        raise SPDXParsingError(
+            [f'Could not convert str to datetime, format of {date_str} does not match "%Y-%m-%dT%H:%M:%SZ"'])
+    return date
+
+
+def json_str_to_enum_name(json_str: str) -> str:
+    if not isinstance(json_str, str):
+        raise SPDXParsingError([f"Type for enum must be str not {type(json_str).__name__}"])
+    return json_str.replace("-", "_").upper()
+
+
+def construct_or_raise_parsing_error(object_to_construct: Any, args_for_construction: Dict) -> Any:
+    try:
+        constructed_object = object_to_construct(**args_for_construction)
+    except ConstructorTypeErrors as err:
+        raise SPDXParsingError([f"Error while constructing {object_to_construct.__name__}: {err.get_messages()}"])
+    return constructed_object
+
+
+def parse_field_or_log_error(logger: Logger, field: Any, parsing_method: Callable = lambda x: x, default: Any = None,
+                             field_is_list: bool = False) -> Any:
+    if not field:
+        return default
+    try:
+        if field_is_list:
+            return parse_list_of_elements(field, parsing_method)
+        else:
+            return parsing_method(field)
+    except SPDXParsingError as err:
+        logger.extend(err.get_messages())
+        return default
+
+
+def append_parsed_field_or_log_error(logger: Logger, list_to_append_to: List[Any], field: Any,
+                                     method_to_parse: Callable) -> List[Any]:
+    try:
+        parsed_element = method_to_parse(field)
+        list_to_append_to.append(parsed_element)
+    except SPDXParsingError as err:
+        logger.extend(err.get_messages())
+    return list_to_append_to
+
+
+def raise_parsing_error_if_logger_has_messages(logger: Logger, parsed_object_name: str = None):
+    if logger.has_messages():
+        if parsed_object_name:
+            raise SPDXParsingError([f"Error while parsing {parsed_object_name}: {logger.get_messages()}"])
+        else:
+            raise SPDXParsingError(logger.get_messages())
+
+
+def parse_field_or_no_assertion_or_none(field: Optional[str], method_for_field: Callable = lambda x: x) -> Any:
+    if field == SpdxNoAssertion().__str__():
+        return SpdxNoAssertion()
+    elif field == SpdxNone().__str__():
+        return SpdxNone()
+    else:
+        return method_for_field(field)
+
+
+def parse_field_or_no_assertion(field: Optional[str], method_for_field: Callable = lambda x: x) -> Any:
+    if field == SpdxNoAssertion().__str__():
+        return SpdxNoAssertion()
+    else:
+        return method_for_field(field)
+
+
+def parse_list_of_elements(list_of_elements: List[Dict], method_to_parse_element: Callable, logger=None) -> List[Any]:
+    if not logger:
+        logger = Logger()
+    parsed_elements = []
+    for element_dict in list_of_elements:
+        parsed_elements = append_parsed_field_or_log_error(logger, parsed_elements, element_dict,
+                                                           method_to_parse_element)
+    raise_parsing_error_if_logger_has_messages(logger)
+    return parsed_elements
diff --git a/src/parser/json/extracted_licensing_info_parser.py b/src/parser/json/extracted_licensing_info_parser.py
new file mode 100644
index 0000000..dcb4360
--- /dev/null
+++ b/src/parser/json/extracted_licensing_info_parser.py
@@ -0,0 +1,39 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Dict, List, Optional, Union
+
+from src.model.extracted_licensing_info import ExtractedLicensingInfo
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.parser.json.dict_parsing_functions import construct_or_raise_parsing_error, parse_field_or_no_assertion
+from src.parser.logger import Logger
+
+
+class ExtractedLicensingInfoParser:
+    logger: Logger
+
+    def __init__(self):
+        self.logger = Logger()
+
+    @staticmethod
+    def parse_extracted_licensing_info(extracted_licensing_info_dict: Dict) -> ExtractedLicensingInfo:
+        license_id: Optional[str] = extracted_licensing_info_dict.get("licenseId")
+        extracted_text: Optional[str] = extracted_licensing_info_dict.get("extractedText")
+        license_name: Optional[Union[str, SpdxNoAssertion]] = parse_field_or_no_assertion(
+            extracted_licensing_info_dict.get("name"))
+        cross_references: List[str] = extracted_licensing_info_dict.get("seeAlsos", [])
+        comment: Optional[str] = extracted_licensing_info_dict.get("comment")
+        extracted_licensing_info = construct_or_raise_parsing_error(ExtractedLicensingInfo,
+                                                                    dict(license_id=license_id,
+                                                                         extracted_text=extracted_text,
+                                                                         comment=comment,
+                                                                         license_name=license_name,
+                                                                         cross_references=cross_references))
+        return extracted_licensing_info
diff --git a/src/parser/json/file_parser.py b/src/parser/json/file_parser.py
new file mode 100644
index 0000000..e9c236a
--- /dev/null
+++ b/src/parser/json/file_parser.py
@@ -0,0 +1,85 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Dict, List, Optional, Union
+
+from src.model.checksum import Checksum
+from src.model.file import File, FileType
+from src.model.license_expression import LicenseExpression
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
+from src.parser.json.checksum_parser import ChecksumParser
+from src.parser.json.dict_parsing_functions import raise_parsing_error_if_logger_has_messages, \
+    construct_or_raise_parsing_error, parse_field_or_log_error, \
+    parse_field_or_no_assertion_or_none, parse_list_of_elements
+from src.parser.json.license_expression_parser import LicenseExpressionParser
+from src.parser.logger import Logger
+
+
+class FileParser:
+    logger: Logger
+    checksum_parser: ChecksumParser
+    license_expression_parser: LicenseExpressionParser
+
+    def __init__(self):
+        self.logger = Logger()
+        self.checksum_parser = ChecksumParser()
+        self.license_expression_parser = LicenseExpressionParser()
+
+    def parse_file(self, file_dict: Dict) -> Optional[File]:
+        logger = Logger()
+        name: Optional[str] = file_dict.get("fileName")
+        spdx_id: Optional[str] = file_dict.get("SPDXID")
+        checksums_list: List[Dict] = file_dict.get("checksums")
+        checksums: List[Checksum] = parse_field_or_log_error(logger, checksums_list, self.checksum_parser.parse_checksum, field_is_list=True)
+
+        attribution_texts: List[str] = file_dict.get("attributionTexts", [])
+        comment: Optional[str] = file_dict.get("comment")
+        copyright_text: Optional[str] = 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)
+
+        license_comments: Optional[str] = file_dict.get("licenseComments")
+
+        license_concluded: Optional[Union[LicenseExpression, SpdxNoAssertion, SpdxNone]] = parse_field_or_log_error(
+            logger, file_dict.get("licenseConcluded"),
+            lambda x: parse_field_or_no_assertion_or_none(x, self.license_expression_parser.parse_license_expression))
+
+        license_info_in_files: Optional[
+            Union[List[LicenseExpression], SpdxNoAssertion, SpdxNone]] = parse_field_or_log_error(
+            logger, file_dict.get("licenseInfoInFiles"),
+            lambda x: parse_field_or_no_assertion_or_none(x, self.license_expression_parser.parse_license_expressions))
+        notice_text: Optional[str] = file_dict.get("noticeText")
+        raise_parsing_error_if_logger_has_messages(logger, "File")
+
+        file = construct_or_raise_parsing_error(File, dict(name=name, spdx_id=spdx_id, checksums=checksums,
+                                                           attribution_texts=attribution_texts, comment=comment,
+                                                           copyright_text=copyright_text, file_type=file_types,
+                                                           contributors=file_contributors,
+                                                           license_comment=license_comments,
+                                                           concluded_license=license_concluded,
+                                                           license_info_in_file=license_info_in_files,
+                                                           notice=notice_text)
+                                                )
+        return file
+
+    @staticmethod
+    def parse_file_types(file_types_list: List[str]) -> List[FileType]:
+        logger = Logger()
+        file_types = []
+        for file_type in file_types_list:
+            try:
+                file_type = FileType[file_type]
+            except KeyError:
+                logger.append(f"Invalid FileType: {file_type}")
+                continue
+            file_types.append(file_type)
+        raise_parsing_error_if_logger_has_messages(logger, "FileType")
+        return file_types
diff --git a/src/parser/json/json_parser.py b/src/parser/json/json_parser.py
new file mode 100644
index 0000000..e81e716
--- /dev/null
+++ b/src/parser/json/json_parser.py
@@ -0,0 +1,83 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import json
+
+from src.model.document import Document
+from src.parser.error import SPDXParsingError
+from src.parser.json.annotation_parser import AnnotationParser
+from src.parser.json.creation_info_parser import CreationInfoParser
+from src.parser.json.dict_parsing_functions import raise_parsing_error_if_logger_has_messages, \
+    construct_or_raise_parsing_error, parse_list_of_elements
+from src.parser.json.extracted_licensing_info_parser import ExtractedLicensingInfoParser
+from src.parser.json.file_parser import FileParser
+from src.parser.logger import Logger
+from src.parser.json.package_parser import PackageParser
+from src.parser.json.relationship_parser import RelationshipParser
+from src.parser.json.snippet_parser import SnippetParser
+
+
+class JsonParser:
+    logger: Logger
+    creation_info_parser: CreationInfoParser
+    package_parser: PackageParser
+    file_parser: FileParser
+    snippet_parser: SnippetParser
+    extracted_licensing_info_parser: ExtractedLicensingInfoParser
+    relationship_parser: RelationshipParser
+    annotation_parser: AnnotationParser
+
+    def __init__(self):
+        self.logger = Logger()
+        self.creation_info_parser = CreationInfoParser()
+        self.package_parser = PackageParser()
+        self.file_parser = FileParser()
+        self.snippet_parser = SnippetParser()
+        self.extracted_licensing_info_parser = ExtractedLicensingInfoParser()
+        self.relationship_parser = RelationshipParser()
+        self.annotation_parser = AnnotationParser()
+
+    def parse(self, filename: str) -> Document:
+
+        with open(filename) as file:
+            input_doc_as_dict = json.load(file)
+
+        fields_to_parse = [("creation_info", input_doc_as_dict, self.creation_info_parser.parse_creation_info, False),
+                           ("packages", input_doc_as_dict.get("packages"), lambda x: parse_list_of_elements(x,
+                                                                                                            self.package_parser.parse_package,
+                                                                                                            self.package_parser.logger), True),
+                           ("files", input_doc_as_dict.get("files"), lambda x: parse_list_of_elements(x,
+                                                                                                      self.file_parser.parse_file,
+                                                                                                      self.file_parser.logger), True),
+                           ("annotations", input_doc_as_dict, self.annotation_parser.parse_all_annotations, True),
+                           ("snippets", input_doc_as_dict.get("snippets"), lambda x: parse_list_of_elements(x,
+                                                                                                            self.snippet_parser.parse_snippet,
+                                                                                                            self.snippet_parser.logger), True),
+                           ("relationships", input_doc_as_dict, self.relationship_parser.parse_all_relationships, True),
+                           ("extracted_licensing_info", input_doc_as_dict.get("hasExtractedLicensingInfos"),
+                            lambda x: parse_list_of_elements(x,
+                                                             self.extracted_licensing_info_parser.parse_extracted_licensing_info,
+                                                             self.extracted_licensing_info_parser.logger), True)]
+
+        parsed_fields = {}
+
+        for argument_name, field, parsing_method, optional in fields_to_parse:
+            if optional and not field:
+                continue
+            try:
+                parsed_fields[argument_name] = parsing_method(field)
+            except SPDXParsingError as err:
+                self.logger.extend(err.get_messages())
+
+        raise_parsing_error_if_logger_has_messages(self.logger)
+
+        document = construct_or_raise_parsing_error(Document, parsed_fields)
+
+        return document
diff --git a/src/parser/json/license_expression_parser.py b/src/parser/json/license_expression_parser.py
new file mode 100644
index 0000000..5fc0df4
--- /dev/null
+++ b/src/parser/json/license_expression_parser.py
@@ -0,0 +1,40 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Union, List
+
+from src.model.license_expression import LicenseExpression
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import construct_or_raise_parsing_error, append_parsed_field_or_log_error, \
+    raise_parsing_error_if_logger_has_messages
+from src.parser.logger import Logger
+
+
+class LicenseExpressionParser:
+    @staticmethod
+    def parse_license_expression(license_expression_str_or_list: str) -> LicenseExpression:
+        license_expression = construct_or_raise_parsing_error(LicenseExpression,
+                                                              dict(expression_string=license_expression_str_or_list))
+        return license_expression
+
+    def parse_license_expressions(self, license_expression_str_or_list: Union[str, List[str]]) -> Union[LicenseExpression, List[LicenseExpression]]:
+        if isinstance(license_expression_str_or_list, str):
+            return self.parse_license_expression(license_expression_str_or_list)
+        license_expressions = []
+        logger = Logger()
+        for license_expression_str in license_expression_str_or_list:
+            try:
+                license_expressions = append_parsed_field_or_log_error(logger, license_expressions,
+                                                                       license_expression_str,
+                                                                       self.parse_license_expression)
+            except SPDXParsingError as err:
+                logger.append(err.get_messages())
+        raise_parsing_error_if_logger_has_messages(logger)
+        return license_expressions
diff --git a/src/parser/json/package_parser.py b/src/parser/json/package_parser.py
new file mode 100644
index 0000000..eb7118d
--- /dev/null
+++ b/src/parser/json/package_parser.py
@@ -0,0 +1,172 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from datetime import datetime
+from typing import Dict, List, Optional, Union
+
+from src.model.actor import Actor
+from src.model.license_expression import LicenseExpression
+from src.model.package import Package, ExternalPackageRef, PackageVerificationCode, PackagePurpose, \
+    ExternalPackageRefCategory
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
+from src.parser.error import SPDXParsingError
+from src.parser.json.actor_parser import ActorParser
+from src.parser.json.checksum_parser import ChecksumParser
+from src.parser.json.dict_parsing_functions import append_parsed_field_or_log_error, datetime_from_str, \
+    raise_parsing_error_if_logger_has_messages, json_str_to_enum_name, construct_or_raise_parsing_error, \
+    parse_field_or_log_error, parse_field_or_no_assertion_or_none, parse_field_or_no_assertion, parse_list_of_elements
+from src.parser.json.license_expression_parser import LicenseExpressionParser
+from src.parser.logger import Logger
+
+
+class PackageParser:
+    logger: Logger
+    actor_parser: ActorParser
+    checksum_parser: ChecksumParser
+    license_expression_parser: LicenseExpressionParser
+
+    def __init__(self):
+        self.actor_parser = ActorParser()
+        self.checksum_parser = ChecksumParser()
+        self.license_expression_parser = LicenseExpressionParser()
+        self.logger = Logger()
+
+    def parse_package(self, package_dict: Dict) -> Package:
+        logger = Logger()
+        name: Optional[str] = package_dict.get("name")
+        spdx_id: Optional[str] = package_dict.get("SPDXID")
+        attribution_texts: List[str] = package_dict.get("attributionTexts", [])
+
+        built_date: Optional[datetime] = parse_field_or_log_error(logger, package_dict.get("builtDate"),
+                                                                  datetime_from_str)
+
+        checksums = parse_field_or_log_error(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")
+        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"))
+
+        external_refs: List[ExternalPackageRef] = parse_field_or_log_error(logger, package_dict.get("externalRefs"),
+                                                                           self.parse_external_refs)
+
+        files_analyzed: Optional[bool] = parse_field_or_log_error(logger, package_dict.get("filesAnalyzed"),
+                                                                  lambda x: x, True)
+        homepage: Optional[str] = package_dict.get("homepage")
+        license_comments: Optional[str] = package_dict.get("licenseComments")
+        license_concluded = parse_field_or_log_error(
+            logger, package_dict.get("licenseConcluded"),
+            lambda x: parse_field_or_no_assertion_or_none(x, self.license_expression_parser.parse_license_expression),
+            None)
+
+        license_declared: Optional[Union[LicenseExpression, SpdxNoAssertion, SpdxNone]] = parse_field_or_log_error(
+            logger, package_dict.get("licenseDeclared"),
+            lambda x: parse_field_or_no_assertion_or_none(x, self.license_expression_parser.parse_license_expression))
+
+        license_info_from_file: Optional[Union[List[LicenseExpression], SpdxNoAssertion, SpdxNone]] = \
+            parse_field_or_log_error(
+                logger, package_dict.get("licenseInfoFromFiles"),
+                lambda x: parse_field_or_no_assertion_or_none(x,
+                                                              self.license_expression_parser.parse_license_expressions))
+        originator: Optional[Union[Actor, SpdxNoAssertion]] = parse_field_or_log_error(
+            logger, package_dict.get("originator"),
+            lambda x: parse_field_or_no_assertion(x, self.actor_parser.parse_actor))
+        package_file_name: Optional[str] = package_dict.get("packageFileName")
+
+        package_verification_code: Optional[
+            PackageVerificationCode] = parse_field_or_log_error(logger, package_dict.get("packageVerificationCode"),
+                                                                self.parse_package_verification_code)
+        primary_package_purpose: Optional[PackagePurpose] = parse_field_or_log_error(logger, package_dict.get(
+            "primaryPackagePurpose"), self.parse_primary_package_purpose)
+
+        release_date: Optional[datetime] = parse_field_or_log_error(logger, package_dict.get("releaseDate"),
+                                                                    datetime_from_str)
+        source_info: Optional[str] = package_dict.get("sourceInfo")
+        summary: Optional[str] = package_dict.get("summary")
+        supplier: Optional[Union[Actor, SpdxNoAssertion]] = parse_field_or_log_error(
+            logger, package_dict.get("supplier"),
+            lambda x: parse_field_or_no_assertion(x, self.actor_parser.parse_actor))
+        valid_until_date: Optional[datetime] = parse_field_or_log_error(logger, package_dict.get("validUntilDate"),
+                                                                        datetime_from_str)
+
+        version_info: Optional[str] = package_dict.get("versionInfo")
+        raise_parsing_error_if_logger_has_messages(logger, "Package")
+
+        package = construct_or_raise_parsing_error(Package,
+                                                   dict(spdx_id=spdx_id, name=name, download_location=download_location,
+                                                        version=version_info, file_name=package_file_name,
+                                                        supplier=supplier, originator=originator,
+                                                        files_analyzed=files_analyzed,
+                                                        verification_code=package_verification_code,
+                                                        checksums=checksums, homepage=homepage, source_info=source_info,
+                                                        license_concluded=license_concluded,
+                                                        license_info_from_files=license_info_from_file,
+                                                        license_declared=license_declared,
+                                                        license_comment=license_comments,
+                                                        copyright_text=copyright_text, summary=summary,
+                                                        description=description, comment=comment,
+                                                        external_references=external_refs,
+                                                        attribution_texts=attribution_texts,
+                                                        primary_package_purpose=primary_package_purpose,
+                                                        release_date=release_date, built_date=built_date,
+                                                        valid_until_date=valid_until_date))
+
+        return package
+
+    def parse_external_refs(self, external_ref_dicts: List[Dict]) -> List[ExternalPackageRef]:
+        external_refs = []
+        for external_ref_dict in external_ref_dicts:
+            external_refs = append_parsed_field_or_log_error(self.logger, external_refs, external_ref_dict,
+                                                             self.parse_external_ref)
+        return external_refs
+
+    def parse_external_ref(self, external_ref_dict: Dict) -> ExternalPackageRef:
+        logger = Logger()
+        ref_category = parse_field_or_log_error(logger, external_ref_dict.get("referenceCategory"),
+                                                self.parse_external_ref_category)
+        ref_locator: Optional[str] = external_ref_dict.get("referenceLocator")
+        ref_type: Optional[str] = external_ref_dict.get("referenceType")
+        comment: Optional[str] = external_ref_dict.get("comment")
+        raise_parsing_error_if_logger_has_messages(logger, "ExternalPackageRef")
+        external_ref = construct_or_raise_parsing_error(ExternalPackageRef,
+                                                        dict(category=ref_category, reference_type=ref_type,
+                                                             locator=ref_locator, comment=comment))
+
+        return external_ref
+
+    @staticmethod
+    def parse_external_ref_category(external_ref_category_str: str) -> ExternalPackageRefCategory:
+        try:
+            external_ref_category = ExternalPackageRefCategory[
+                json_str_to_enum_name(external_ref_category_str)]
+        except KeyError:
+            raise SPDXParsingError([f"Invalid ExternalPackageRefCategory: {external_ref_category_str}"])
+
+        return external_ref_category
+
+    @staticmethod
+    def parse_package_verification_code(verification_code_dict: Dict) -> PackageVerificationCode:
+        excluded_files: List[str] = verification_code_dict.get("packageVerificationCodeExcludedFiles", [])
+        verification_code_value: Optional[str] = verification_code_dict.get("packageVerificationCodeValue")
+
+        package_verification_code = construct_or_raise_parsing_error(PackageVerificationCode,
+                                                                     dict(value=verification_code_value,
+                                                                          excluded_files=excluded_files))
+
+        return package_verification_code
+
+    @staticmethod
+    def parse_primary_package_purpose(primary_package_purpose: str) -> PackagePurpose:
+        try:
+            return PackagePurpose[json_str_to_enum_name(primary_package_purpose)]
+        except KeyError:
+            raise SPDXParsingError([f"Invalid PrimaryPackagePurpose: {primary_package_purpose}"])
diff --git a/src/parser/json/relationship_parser.py b/src/parser/json/relationship_parser.py
new file mode 100644
index 0000000..defecd3
--- /dev/null
+++ b/src/parser/json/relationship_parser.py
@@ -0,0 +1,165 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import Dict, List, Optional
+
+from src.model.relationship import Relationship, RelationshipType
+from src.model.typing.constructor_type_errors import ConstructorTypeErrors
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import raise_parsing_error_if_logger_has_messages, json_str_to_enum_name, \
+    construct_or_raise_parsing_error, \
+    parse_field_or_log_error, parse_list_of_elements
+from src.parser.logger import Logger
+
+
+class RelationshipParser:
+    logger: Logger
+
+    def __init__(self):
+        self.logger = Logger()
+
+    def parse_all_relationships(self, input_doc_dict: Dict) -> List[Relationship]:
+        relationships = []
+        relationship_dicts: List[Dict] = input_doc_dict.get("relationships", [])
+        relationships.extend(
+            parse_field_or_log_error(self.logger, relationship_dicts, self.parse_relationship, [], True))
+
+        document_describes: List[str] = input_doc_dict.get("documentDescribes", [])
+        doc_spdx_id: Optional[str] = input_doc_dict.get("SPDXID")
+
+        relationships.extend(
+            parse_field_or_log_error(self.logger, document_describes, lambda x: self.parse_document_describes(
+                doc_spdx_id=doc_spdx_id, described_spdx_ids=x,
+                existing_relationships=relationships), []))
+
+        package_dicts: List[Dict] = input_doc_dict.get("packages", [])
+
+        relationships.extend(parse_field_or_log_error(
+            self.logger, package_dicts,
+            lambda x: self.parse_has_files(package_dicts=x, existing_relationships=relationships), []))
+
+        file_dicts: List[Dict] = input_doc_dict.get("files", [])
+
+        # not implemented yet: deal with deprecated fields in file: https://github.com/spdx/tools-python/issues/294 & https://github.com/spdx/tools-python/issues/387
+        generated_relationships = self.parse_artifact_of(file_dicts=file_dicts)
+        dependency_relationships = self.parse_file_dependencies(file_dicts=file_dicts)
+
+        raise_parsing_error_if_logger_has_messages(self.logger)
+
+        return relationships
+
+    def parse_relationship(self, relationship_dict: Dict) -> Relationship:
+        logger = Logger()
+        spdx_element_id: Optional[str] = relationship_dict.get("spdxElementId")
+        related_spdx_element: Optional[str] = relationship_dict.get("relatedSpdxElement")
+        relationship_type: Optional[RelationshipType] = parse_field_or_log_error(logger, relationship_dict.get(
+            "relationshipType"), self.parse_relationship_type)
+        relationship_comment: Optional[str] = relationship_dict.get("comment")
+        raise_parsing_error_if_logger_has_messages(logger, "Relationship")
+
+        relationship = construct_or_raise_parsing_error(Relationship, dict(spdx_element_id=spdx_element_id,
+                                                                           relationship_type=relationship_type,
+                                                                           related_spdx_element_id=related_spdx_element,
+                                                                           comment=relationship_comment))
+        return relationship
+
+    @staticmethod
+    def parse_relationship_type(relationship_type_str: str) -> RelationshipType:
+        try:
+            relationship_type = RelationshipType[json_str_to_enum_name(relationship_type_str)]
+        except KeyError:
+            raise SPDXParsingError([f"Invalid RelationshipType: {relationship_type_str}"])
+        return relationship_type
+
+    def parse_document_describes(self, doc_spdx_id: str, described_spdx_ids: List[str],
+                                 existing_relationships: List[Relationship]) -> List[Relationship]:
+        logger = Logger()
+        describes_relationships = []
+        for spdx_id in described_spdx_ids:
+            try:
+                describes_relationship = Relationship(spdx_element_id=doc_spdx_id,
+                                                      relationship_type=RelationshipType.DESCRIBES,
+                                                      related_spdx_element_id=spdx_id)
+            except ConstructorTypeErrors as err:
+                logger.append(err.get_messages())
+                continue
+            if not self.check_if_relationship_exists(describes_relationship, existing_relationships):
+                describes_relationships.append(describes_relationship)
+        raise_parsing_error_if_logger_has_messages(logger, "document describes relationships")
+
+        return describes_relationships
+
+    def parse_has_files(self, package_dicts: List[Dict], existing_relationships: List[Relationship]) -> List[
+        Relationship]:
+        logger = Logger()
+        contains_relationships = []
+        for package in package_dicts:
+            package_spdx_id: Optional[str] = package.get("SPDXID")
+            contained_files: Optional[str] = package.get("hasFiles")
+            if not contained_files:
+                continue
+            for file_spdx_id in contained_files:
+                try:
+                    contains_relationship = Relationship(spdx_element_id=package_spdx_id,
+                                                         relationship_type=RelationshipType.CONTAINS,
+                                                         related_spdx_element_id=file_spdx_id)
+                except ConstructorTypeErrors as err:
+                    logger.append(err.get_messages())
+                    continue
+                if not self.check_if_relationship_exists(relationship=contains_relationship,
+                                                         existing_relationships=existing_relationships):
+                    contains_relationships.append(contains_relationship)
+        raise_parsing_error_if_logger_has_messages(logger, "package contains relationships")
+
+        return contains_relationships
+
+    def check_if_relationship_exists(self, relationship: Relationship,
+                                     existing_relationships: List[Relationship]) -> bool:
+        existing_relationships_without_comments: List[Relationship] = self.get_all_relationships_without_comments(
+            existing_relationships)
+        if relationship in existing_relationships_without_comments:
+            return True
+        relationship_inverted: Relationship = self.invert_relationship(relationship)
+        if relationship_inverted in existing_relationships_without_comments:
+            return True
+
+        return False
+
+    @staticmethod
+    def get_all_relationships_without_comments(existing_relationships: List[Relationship]) -> List[Relationship]:
+        relationships_without_comments = [Relationship(relationship_type=relationship.relationship_type,
+                                                       related_spdx_element_id=relationship.related_spdx_element_id,
+                                                       spdx_element_id=relationship.spdx_element_id) for relationship in
+                                          existing_relationships]
+        return relationships_without_comments
+
+    def invert_relationship(self, relationship: Relationship) -> Relationship:
+        return Relationship(related_spdx_element_id=relationship.spdx_element_id,
+                            spdx_element_id=relationship.related_spdx_element_id,
+                            relationship_type=self.invert_relationship_types[relationship.relationship_type],
+                            comment=relationship.comment)
+
+    invert_relationship_types = {RelationshipType.DESCRIBES: RelationshipType.DESCRIBED_BY,
+                                 RelationshipType.DESCRIBED_BY: RelationshipType.DESCRIBES,
+                                 RelationshipType.CONTAINS: RelationshipType.CONTAINED_BY,
+                                 RelationshipType.CONTAINED_BY: RelationshipType.CONTAINS}
+
+    @staticmethod
+    def parse_file_dependencies(file_dicts: List[Dict]) -> List[
+        Relationship]:
+        dependency_relationships = []
+        # the field fileDependencies is deprecated and should be converted to a relationship (https://github.com/spdx/tools-python/issues/387)
+        return dependency_relationships
+
+    @staticmethod
+    def parse_artifact_of(file_dicts: List[Dict]) -> List[Relationship]:
+        generated_relationships = []
+        # artifactOfs is deprecated and should be converted to an external package and a generated from relationship (https://github.com/spdx/tools-python/issues/294)
+        return generated_relationships
diff --git a/src/parser/json/snippet_parser.py b/src/parser/json/snippet_parser.py
new file mode 100644
index 0000000..97ab663
--- /dev/null
+++ b/src/parser/json/snippet_parser.py
@@ -0,0 +1,116 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from enum import auto, Enum
+from typing import Dict, Tuple, List, Optional, Union
+
+from src.model.license_expression import LicenseExpression
+from src.model.snippet import Snippet
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import construct_or_raise_parsing_error, parse_field_or_log_error, \
+    parse_field_or_no_assertion_or_none
+
+from src.parser.json.license_expression_parser import LicenseExpressionParser
+from src.parser.logger import Logger
+
+
+class RangeType(Enum):
+    BYTE = auto()
+    LINE = auto()
+
+
+class SnippetParser:
+    logger: Logger
+    license_expression_parser = LicenseExpressionParser
+
+    def __init__(self):
+        self.logger = Logger()
+        self.license_expression_parser = LicenseExpressionParser()
+
+    def parse_snippet(self, snippet_dict: Dict) -> Snippet:
+        logger = Logger()
+        spdx_id: Optional[str] = snippet_dict.get("SPDXID")
+        file_spdx_id: Optional[str] = snippet_dict.get("snippetFromFile")
+        name: Optional[str] = snippet_dict.get("name")
+        ranges: Dict = parse_field_or_log_error(logger, snippet_dict.get("ranges", []), self.parse_ranges, default={})
+        byte_range: Tuple[int, int] = ranges.get(RangeType.BYTE)
+        line_range: Optional[Tuple[int, int]] = ranges.get(RangeType.LINE)
+        attribution_texts: List[str] = snippet_dict.get("attributionTexts", [])
+        comment: Optional[str] = snippet_dict.get("comment")
+        copyright_text: Optional[str] = snippet_dict.get("copyrightText")
+        license_comment: Optional[str] = snippet_dict.get("licenseComments")
+        concluded_license: Optional[Union[
+            LicenseExpression, SpdxNoAssertion, SpdxNone]] = parse_field_or_log_error(logger, snippet_dict.get(
+            "licenseConcluded"), lambda x: parse_field_or_no_assertion_or_none(x,
+                                                                               self.license_expression_parser.parse_license_expression))
+
+        license_info: Optional[Union[List[
+            LicenseExpression], SpdxNoAssertion, SpdxNone]] = parse_field_or_log_error(logger, snippet_dict.get(
+            "licenseInfoInSnippets"), lambda x: parse_field_or_no_assertion_or_none(x,
+                                                                                    self.license_expression_parser.parse_license_expressions))
+        if logger.has_messages():
+            raise SPDXParsingError([f"Error while parsing snippet: {logger.get_messages()}"])
+
+        snippet = construct_or_raise_parsing_error(Snippet,
+                                                   dict(spdx_id=spdx_id, name=name, byte_range=byte_range,
+                                                        file_spdx_id=file_spdx_id, line_range=line_range,
+                                                        attribution_texts=attribution_texts, comment=comment,
+                                                        copyright_text=copyright_text, license_comment=license_comment,
+                                                        concluded_license=concluded_license,
+                                                        license_info_in_snippet=license_info))
+
+        return snippet
+
+    def parse_ranges(self, ranges_from_snippet: List[Dict]) -> Dict:
+        logger = Logger()
+        ranges = {}
+        for range_dict in ranges_from_snippet:
+            try:
+                range_type: RangeType = self.validate_range_and_get_type(range_dict)
+                start_end_tuple: Tuple[int, int] = SnippetParser.get_start_end_tuple(range_dict, range_type)
+                ranges[range_type] = start_end_tuple
+            except ValueError as error:
+                logger.append(error.args[0])
+        if logger.has_messages():
+            raise SPDXParsingError([f"Error while parsing snippet ranges: {logger.get_messages()}"])
+        return ranges
+
+    @staticmethod
+    def get_start_end_tuple(range_dict: Dict, range_type: RangeType) -> Tuple[int, int]:
+        end_pointer: Dict = range_dict["endPointer"]
+        start_pointer: Dict = range_dict["startPointer"]
+        if range_type == RangeType.BYTE:
+            start: int = start_pointer["offset"]
+            end: int = end_pointer["offset"]
+        else:
+            start: int = start_pointer["lineNumber"]
+            end: int = end_pointer["lineNumber"]
+        return start, end
+
+    def validate_range_and_get_type(self, range_dict: Dict) -> RangeType:
+        if "startPointer" not in range_dict:
+            raise ValueError("Startpointer missing in snippet ranges.")
+        if "endPointer" not in range_dict:
+            raise ValueError("Endpointer missing in snippet ranges.")
+        start_pointer_type: RangeType = self.validate_pointer_and_get_type(range_dict["startPointer"])
+        end_pointer_type: RangeType = self.validate_pointer_and_get_type(range_dict["endPointer"])
+        if start_pointer_type != end_pointer_type:
+            raise ValueError("Type of startpointer is not the same as type of endpointer.")
+        return start_pointer_type
+
+    @staticmethod
+    def validate_pointer_and_get_type(pointer: Dict) -> RangeType:
+        if "offset" in pointer and "lineNumber" in pointer:
+            raise ValueError('Couldn\'t determine type of pointer: "offset" and "lineNumber" provided as key.')
+        if "offset" not in pointer and "lineNumber" not in pointer:
+            raise ValueError('Couldn\'t determine type of pointer: neither "offset" nor "lineNumber" provided as key.')
+        return RangeType.BYTE if "offset" in pointer else RangeType.LINE
diff --git a/src/parser/logger.py b/src/parser/logger.py
new file mode 100644
index 0000000..312a959
--- /dev/null
+++ b/src/parser/logger.py
@@ -0,0 +1,30 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from typing import List
+
+
+class Logger:
+    messages: List[str]
+
+    def __init__(self):
+        self.messages = []
+
+    def append(self, message: str):
+        self.messages.append(message)
+
+    def extend(self, messages_to_append: List[str]):
+        self.messages.extend(messages_to_append)
+
+    def has_messages(self):
+        return bool(self.messages)
+
+    def get_messages(self):
+        return list(self.messages)


Test patch:
diff --git a/tests/parser/__init__.py b/tests/parser/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/parser/test_actor_parser.py b/tests/parser/test_actor_parser.py
new file mode 100644
index 0000000..d73376c
--- /dev/null
+++ b/tests/parser/test_actor_parser.py
@@ -0,0 +1,48 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import pytest
+from unittest import TestCase
+
+from src.model.actor import ActorType
+from src.parser.error import SPDXParsingError
+from src.parser.json.actor_parser import ActorParser
+
+
+@pytest.mark.parametrize("actor_string,expected_type,expected_name,expected_mail", [
+    ("Person: Jane Doe (jane.doe@example.com)", ActorType.PERSON, "Jane Doe", "jane.doe@example.com"),
+    ("Organization: Example organization (organization@example.com)", ActorType.ORGANIZATION, "Example organization",
+     "organization@example.com"),
+    ("Organization: Example organization ( )", ActorType.ORGANIZATION, "Example organization", None),
+    ("Tool: Example tool ", ActorType.TOOL, "Example tool", None)])
+def test_parse_actor(actor_string, expected_type, expected_name, expected_mail):
+    actor_parser = ActorParser()
+
+    actor = actor_parser.parse_actor(actor_string)
+
+    assert actor.actor_type == expected_type
+    assert actor.name == expected_name
+    assert actor.email == expected_mail
+
+
+@pytest.mark.parametrize("actor_string,expected_message", [
+    ("Perso: Jane Doe (jane.doe@example.com)",
+     ["Actor Perso: Jane Doe (jane.doe@example.com) doesn't match any of person, organization or tool."]),
+    ("Toole Example Tool ()",
+     ["Actor Toole Example Tool () doesn't match any of person, organization or tool."])
+])
+def test_parse_invalid_actor(actor_string, expected_message):
+    actor_parser = ActorParser()
+    actor_string = actor_string
+
+    with pytest.raises(SPDXParsingError) as err:
+        actor_parser.parse_actor(actor_string)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
diff --git a/tests/parser/test_annotation_parser.py b/tests/parser/test_annotation_parser.py
new file mode 100644
index 0000000..0330b5b
--- /dev/null
+++ b/tests/parser/test_annotation_parser.py
@@ -0,0 +1,116 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+import datetime
+from unittest import TestCase
+
+import pytest
+
+from src.model.actor import Actor, ActorType
+from src.model.annotation import AnnotationType, Annotation
+from src.parser.error import SPDXParsingError
+from src.parser.json.annotation_parser import AnnotationParser
+
+
+def test_parse_annotation():
+    annotation_parser = AnnotationParser()
+    annotation_dict = {
+        "annotationDate": "2010-01-29T18:30:22Z",
+        "annotationType": "OTHER",
+        "annotator": "Person: Jane Doe ()",
+        "comment": "Document level annotation"
+    }
+
+    annotation = annotation_parser.parse_annotation(annotation_dict, spdx_id="SPDXRef-DOCUMENT")
+
+    assert annotation.annotator == Actor(ActorType.PERSON, name="Jane Doe")
+    assert annotation.annotation_type == AnnotationType.OTHER
+    assert annotation.annotation_date == datetime.datetime(2010, 1, 29, 18, 30, 22)
+    assert annotation.annotation_comment == "Document level annotation"
+    assert annotation.spdx_id == "SPDXRef-DOCUMENT"
+
+
+def test_parse_all_annotations():
+    annotation_parser = AnnotationParser()
+    doc_dict = {
+        "SPDXID": "SPDXRef-DOCUMENT",
+        "packages": [
+            {"SPDXID": "SPDXRef-Package",
+             "annotations": [
+                 {"annotationDate": "2010-01-29T17:30:22Z",
+                  "annotationType": "REVIEW",
+                  "annotator": "Person: Mick Doe ()",
+                  "comment": "Package level annotation"}
+             ]}],
+        "files": [
+            {"SPDXID": "SPDXRef-File",
+             "annotations": [
+                 {"annotationDate": "2010-01-29T18:30:22Z",
+                  "annotationType": "OTHER",
+                  "annotator": "Person: Jane Doe ()",
+                  "comment": "File level annotation"}
+             ]}
+        ],
+        "snippets": [
+            {"SPDXID": "SPDXRef-Snippet",
+             "annotations": [
+                 {"annotationDate": "2022-01-29T18:30:32Z",
+                  "annotationType": "REVIEW",
+                  "annotator": "Person: Jonas Rie (jonas@example.com)",
+                  "comment": "Snippet level annotation"}
+             ]}],
+        "revieweds":
+            [{
+                "reviewDate": "2010-01-29T18:30:22Z",
+                "reviewer": "Person: Jane Doe ()",
+                "comment": "Review annotation"
+            }]
+    }
+
+    annotations = annotation_parser.parse_all_annotations(input_doc_dict=doc_dict)
+
+    assert len(annotations) == 4
+    test_case = TestCase()
+    test_case.maxDiff = None
+    test_case.assertCountEqual(annotations, [Annotation(spdx_id="SPDXRef-DOCUMENT",
+                                                         annotation_type=AnnotationType.REVIEW,
+                                                         annotator=Actor(actor_type=ActorType.PERSON, name="Jane Doe",
+                                                                         email=None),
+                                                         annotation_date=datetime.datetime(2010, 1, 29, 18, 30, 22),
+                                                         annotation_comment="Review annotation"),
+                                              Annotation(spdx_id="SPDXRef-Package",
+                                                         annotation_type=AnnotationType.REVIEW,
+                                                         annotator=Actor(actor_type=ActorType.PERSON, name="Mick Doe",
+                                                                         email=None),
+                                                         annotation_date=datetime.datetime(2010, 1, 29, 17, 30, 22),
+                                                         annotation_comment="Package level annotation"),
+                                              Annotation(spdx_id="SPDXRef-File", annotation_type=AnnotationType.OTHER,
+                                                         annotator=Actor(actor_type=ActorType.PERSON, name="Jane Doe",
+                                                                         email=None),
+                                                         annotation_date=datetime.datetime(2010, 1, 29, 18, 30, 22),
+                                                         annotation_comment="File level annotation"),
+                                              Annotation(spdx_id="SPDXRef-Snippet",
+                                                         annotation_type=AnnotationType.REVIEW,
+                                                         annotator=Actor(actor_type=ActorType.PERSON, name="Jonas Rie",
+                                                                         email="jonas@example.com"),
+                                                         annotation_date=datetime.datetime(2022, 1, 29, 18, 30, 32),
+                                                         annotation_comment="Snippet level annotation")])
+
+
+@pytest.mark.parametrize("incomplete_annotation_dict,expected_message", [({"annotator": "Person: Jane Doe ()"}, [
+    "Error while constructing Annotation: ['SetterError Annotation: type of " 'argument "spdx_id" must be str; got NoneType instead: None\', \'SetterError Annotation: type of argument "annotation_type" must be ' "src.model.annotation.AnnotationType; got NoneType instead: None', " '\'SetterError Annotation: type of argument "annotation_date" must be ' "datetime.datetime; got NoneType instead: None', 'SetterError Annotation: " 'type of argument "annotation_comment" must be str; got NoneType instead: ' "None']"]),
+                                                                         ({"annotationDate": "2010-01-29T18:30:22Z"}, ["Error while constructing Annotation: ['SetterError Annotation: type of " 'argument "spdx_id" must be str; got NoneType instead: None\', \'SetterError Annotation: type of argument "annotation_type" must be ' "src.model.annotation.AnnotationType; got NoneType instead: None', " '\'SetterError Annotation: type of argument "annotator" must be ' "src.model.actor.Actor; got NoneType instead: None', 'SetterError Annotation: " 'type of argument "annotation_comment" must be str; got NoneType instead: ' "None']"])])
+def test_parse_incomplete_annotation(incomplete_annotation_dict, expected_message):
+    annotation_parser = AnnotationParser()
+
+    with pytest.raises(SPDXParsingError) as err:
+        annotation_parser.parse_annotation(incomplete_annotation_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
diff --git a/tests/parser/test_checksum_parser.py b/tests/parser/test_checksum_parser.py
new file mode 100644
index 0000000..11ad161
--- /dev/null
+++ b/tests/parser/test_checksum_parser.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest import TestCase
+
+import pytest
+
+from src.model.checksum import ChecksumAlgorithm
+from src.parser.error import SPDXParsingError
+from src.parser.json.checksum_parser import ChecksumParser
+
+
+def test_parse_checksum():
+    checksum_parser = ChecksumParser()
+    checksum_dict = {
+        "algorithm": "SHA1",
+        "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2759"
+    }
+
+    checksum = checksum_parser.parse_checksum(checksum_dict)
+
+    assert checksum.value == "d6a770ba38583ed4bb4525bd96e50461655d2759"
+    assert checksum.algorithm == ChecksumAlgorithm.SHA1
+
+
+def test_parse_invalid_checksum():
+    checksum_parser = ChecksumParser()
+    checksum_dict = {
+        "algorithm": "SHA",
+        "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2759"
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        checksum_parser.parse_checksum(checksum_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(),
+                                ["Error while parsing Checksum: ['Invalid ChecksumAlgorithm: SHA']"])
+
+
+def test_parse_incomplete_checksum():
+    checksum_parser = ChecksumParser()
+    checksum_dict = {
+        "algorithm": "SHA1"
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        checksum_parser.parse_checksum(checksum_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while constructing Checksum: ['SetterError Checksum: type of argument \"value\" must be str; got NoneType instead: None']"])
diff --git a/tests/parser/test_creation_info_parser.py b/tests/parser/test_creation_info_parser.py
new file mode 100644
index 0000000..8126b7f
--- /dev/null
+++ b/tests/parser/test_creation_info_parser.py
@@ -0,0 +1,105 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from datetime import datetime
+from unittest import TestCase
+
+import pytest
+
+from src.model.actor import Actor, ActorType
+from src.model.checksum import Checksum, ChecksumAlgorithm
+from src.model.external_document_ref import ExternalDocumentRef
+from src.model.version import Version
+from src.parser.error import SPDXParsingError
+from src.parser.json.creation_info_parser import CreationInfoParser
+
+
+def test_parse_creation_info():
+    creation_info_parser = CreationInfoParser()
+    doc_dict = {
+        "spdxVersion": "2.3",
+        "SPDXID": "SPDXRef-DOCUMENT",
+        "name": "Example Document",
+        "dataLicense": "CC0-1.0",
+        "documentNamespace": "namespace",
+        "externalDocumentRefs": [{
+            "externalDocumentId": "DocumentRef-spdx-tool-1.2",
+            "checksum": {
+                "algorithm": "SHA1",
+                "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2759"
+            },
+            "spdxDocument": "http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301"
+        }],
+        "creationInfo": {
+            "created": "2010-01-29T18:30:22Z",
+            "creators": ["Tool: LicenseFind-1.0", "Organization: ExampleCodeInspect ()", "Person: Jane Doe ()"],
+            "licenseListVersion": "3.7",
+            "comment": "Some comment."
+        }
+    }
+    creation_info = creation_info_parser.parse_creation_info(doc_dict)
+
+    assert creation_info.spdx_version == "2.3"
+    assert creation_info.spdx_id == "SPDXRef-DOCUMENT"
+    assert creation_info.name == "Example Document"
+    assert creation_info.document_namespace == "namespace"
+    assert creation_info.created == datetime(2010, 1, 29, 18, 30, 22)
+    TestCase().assertCountEqual(creation_info.creators, [Actor(ActorType.TOOL, "LicenseFind-1.0"),
+                                                         Actor(ActorType.ORGANIZATION, "ExampleCodeInspect"),
+                                                         Actor(ActorType.PERSON, "Jane Doe")])
+    assert creation_info.license_list_version == Version(3, 7)
+    assert creation_info.external_document_refs == [ExternalDocumentRef(document_ref_id="DocumentRef-spdx-tool-1.2",
+                                                                        checksum=Checksum(
+                                                                            algorithm=ChecksumAlgorithm.SHA1,
+                                                                            value="d6a770ba38583ed4bb4525bd96e50461655d2759"),
+                                                                        document_uri="http://spdx.org/spdxdocs/spdx-tools-v1.2-3F2504E0-4F89-41D3-9A0C-0305E82C3301")]
+
+
+@pytest.mark.parametrize("incomplete_dict,expected_message",
+                         [({"spdxVersion": "2.3", "SPDXID": "SPDXRef-DOCUMENT", "name": "Example Document"},
+                           ["Error while parsing document Example Document: ['CreationInfo does not exist.']"]),
+                          ({"creationInfo": {"created": "2019-02-01T11:30:40Z"}},
+                           ["Error while constructing CreationInfo: ['SetterError CreationInfo: type of "
+                            'argument "spdx_version" must be str; got NoneType instead: None\', '
+                            '\'SetterError CreationInfo: type of argument "spdx_id" must be str; got '
+                            "NoneType instead: None', 'SetterError CreationInfo: type of argument "
+                            '"name" must be str; got NoneType instead: None\', \'SetterError '
+                            'CreationInfo: type of argument "document_namespace" must be str; got '
+                            "NoneType instead: None', 'SetterError CreationInfo: type of argument "
+                            '"creators" must be a list; got NoneType instead: None\', \'SetterError '
+                            'CreationInfo: type of argument "data_license" must be str; got NoneType '
+                            "instead: None']"])])
+def test_parse_incomplete_document_info(incomplete_dict, expected_message):
+    creation_info_parser = CreationInfoParser()
+
+    with pytest.raises(SPDXParsingError) as err:
+        creation_info_parser.parse_creation_info(incomplete_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
+
+
+def test_parse_invalid_creation_info():
+    creation_info_parser = CreationInfoParser()
+    doc_dict = {
+        "spdxVersion": "2.3",
+        "SPDXID": "SPDXRef-DOCUMENT",
+        "name": "Example Document",
+        "creationInfo": {
+            "created": "2010-01-29T18:30:22Z",
+            "creators": ["Tool: LicenseFind-1.0", "Organization: ExampleCodeInspect ()", "Person: Jane Doe ()"],
+        },
+        "dataLicense": None
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        creation_info_parser.parse_creation_info(doc_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while constructing CreationInfo: ['SetterError CreationInfo: type of " 'argument "document_namespace" must be str; got NoneType instead: None\', \'SetterError CreationInfo: type of argument "data_license" must be str; got ' "NoneType instead: None']"])
diff --git a/tests/parser/test_dict_parsing_functions.py b/tests/parser/test_dict_parsing_functions.py
new file mode 100644
index 0000000..688f44d
--- /dev/null
+++ b/tests/parser/test_dict_parsing_functions.py
@@ -0,0 +1,71 @@
+#  Copyright (c) 2022 spdx contributors
+#  Licensed under the Apache License, Version 2.0 (the "License");
+#  you may not use this file except in compliance with the License.
+#  You may obtain a copy of the License at
+#    http://www.apache.org/licenses/LICENSE-2.0
+#  Unless required by applicable law or agreed to in writing, software
+#  distributed under the License is distributed on an "AS IS" BASIS,
+#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+#  See the License for the specific language governing permissions and
+#  limitations under the License.
+from datetime import datetime
+from unittest import TestCase
+
+import pytest
+
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.model.spdx_none import SpdxNone
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import datetime_from_str, json_str_to_enum_name, \
+    parse_field_or_no_assertion, parse_field_or_no_assertion_or_none
+
+
+def test_datetime_from_str():
+    date_str = "2010-03-04T05:45:11Z"
+
+    date = datetime_from_str(date_str)
+
+    assert date == datetime(2010, 3, 4, 5, 45, 11)
+
+
+@pytest.mark.parametrize("invalid_date_str,expected_message",
+                         [(5, ["Could not convert str to datetime, invalid type: int"]),
+                          ("2010-02-03", ['Could not convert str to datetime, format of 2010-02-03 does not match '
+                                          '"%Y-%m-%dT%H:%M:%SZ"'])])
+def test_datetime_from_str_error(invalid_date_str, expected_message):
+    with pytest.raises(SPDXParsingError) as err:
+        datetime_from_str(invalid_date_str)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
+
+
+def test_json_str_to_enum():
+    json_str = "BLAKE2b-256"
+
+    enum_name = json_str_to_enum_name(json_str)
+
+    assert enum_name == "BLAKE2B_256"
+
+
+@pytest.mark.parametrize("invalid_json_str,expected_message",
+                         [(5, ["Type for enum must be str not int"])])
+def test_invalid_json_str_to_enum(invalid_json_str, expected_message):
+    with pytest.raises(SPDXParsingError) as err:
+        json_str_to_enum_name(invalid_json_str)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
+
+
+@pytest.mark.parametrize("input_str,expected_type", [("NOASSERTION", SpdxNoAssertion), ("example string", str)])
+def test_parse_field_or_no_assertion(input_str, expected_type):
+    resulting_value = parse_field_or_no_assertion(input_str, lambda x: x)
+
+    assert type(resulting_value) == expected_type
+
+
+@pytest.mark.parametrize("input_str,expected_type",
+                         [("NOASSERTION", SpdxNoAssertion), ("NONE", SpdxNone), ("example string", str)])
+def test_parse_field_or_no_assertion_or_none(input_str, expected_type):
+    resulting_value = parse_field_or_no_assertion_or_none(input_str, lambda x: x)
+
+    assert type(resulting_value) == expected_type
diff --git a/tests/parser/test_extracted_licensing_info_parser.py b/tests/parser/test_extracted_licensing_info_parser.py
new file mode 100644
index 0000000..47f70ef
--- /dev/null
+++ b/tests/parser/test_extracted_licensing_info_parser.py
@@ -0,0 +1,60 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest import TestCase
+
+import pytest
+
+from src.model.spdx_no_assertion import SpdxNoAssertion
+from src.parser.error import SPDXParsingError
+from src.parser.json.extracted_licensing_info_parser import ExtractedLicensingInfoParser
+
+
+def test_parse_extracted_licensing_info():
+    extracted_licensing_info_parser = ExtractedLicensingInfoParser()
+
+    extracted_licensing_infos_dict = {
+
+        "licenseId": "LicenseRef-Beerware-4.2",
+        "comment": "The beerware license has a couple of other standard variants.",
+        "extractedText": "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+        "name": "Beer-Ware License (Version 42)",
+        "seeAlsos": ["http://people.freebsd.org/~phk/"]
+
+    }
+
+    extracted_licensing_info = extracted_licensing_info_parser.parse_extracted_licensing_info(
+        extracted_licensing_infos_dict)
+
+    assert extracted_licensing_info.license_id == "LicenseRef-Beerware-4.2"
+    assert extracted_licensing_info.comment == "The beerware license has a couple of other standard variants."
+    assert extracted_licensing_info.extracted_text == "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp"
+    assert extracted_licensing_info.license_name == "Beer-Ware License (Version 42)"
+    assert extracted_licensing_info.cross_references == ["http://people.freebsd.org/~phk/"]
+
+
+def test_parse_invalid_extracted_licensing_info():
+    extracted_licensing_info_parser = ExtractedLicensingInfoParser()
+
+    extracted_licensing_infos_dict = {
+        "licenseId": "LicenseRef-Beerware-4.2",
+        "comment": 56,
+        "extractedText": "\"THE BEER-WARE LICENSE\" (Revision 42):\nphk@FreeBSD.ORG wrote this file. As long as you retain this notice you\ncan do whatever you want with this stuff. If we meet some day, and you think this stuff is worth it, you can buy me a beer in return Poul-Henning Kamp",
+        "name": "Beer-Ware License (Version 42)",
+        "seeAlsos": ["http://people.freebsd.org/~phk/"]
+
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        extracted_licensing_info_parser.parse_extracted_licensing_info(extracted_licensing_infos_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while constructing ExtractedLicensingInfo: ['SetterError " 'ExtractedLicensingInfo: type of argument "comment" must be one of (str, ' "NoneType); got int instead: 56']"])
+
diff --git a/tests/parser/test_file_parser.py b/tests/parser/test_file_parser.py
new file mode 100644
index 0000000..e3ae52e
--- /dev/null
+++ b/tests/parser/test_file_parser.py
@@ -0,0 +1,129 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest import TestCase
+
+import pytest
+
+from src.model.checksum import Checksum, ChecksumAlgorithm
+from src.model.file import FileType
+from src.model.license_expression import LicenseExpression
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import parse_list_of_elements
+from src.parser.json.file_parser import FileParser
+
+
+def test_parse_file():
+    file_parser = FileParser()
+    file_dict = {
+        "SPDXID": "SPDXRef-File",
+        "attributionTexts": ["Some attribution text."],
+        "checksums": [{
+            "algorithm": "SHA1",
+            "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2758"
+        }, {
+            "algorithm": "MD5",
+            "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+        }],
+        "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",
+        "fileContributors": ["The Regents of the University of California",
+                             "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"],
+        "fileName": "./package/foo.c",
+        "fileTypes": ["SOURCE"],
+        "licenseComments": "The concluded license was taken from the package level that the file was included in.",
+        "licenseConcluded": "(LGPL-2.0-only OR LicenseRef-2)",
+        "licenseInfoInFiles": ["GPL-2.0-only", "LicenseRef-2"],
+        "noticeText": "Copyright (c) 2001 Aaron Lehmann aaroni@vitelus.com\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the \"Software\"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: \nThe above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE."
+    }
+
+    file = file_parser.parse_file(file_dict)
+
+    assert file.name == "./package/foo.c"
+    assert file.spdx_id == "SPDXRef-File"
+    TestCase().assertCountEqual(file.checksums,
+                                [Checksum(ChecksumAlgorithm.SHA1, "d6a770ba38583ed4bb4525bd96e50461655d2758"),
+                                 Checksum(ChecksumAlgorithm.MD5, "624c1abb3664f4b35547e7c73864ad24")])
+    assert 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."
+    assert file.copyright_text == "Copyright 2008-2010 John Smith"
+    assert file.file_type == [FileType.SOURCE]
+    TestCase().assertCountEqual(file.contributors, ["The Regents of the University of California",
+                                                    "Modified by Paul Mundt lethal@linux-sh.org", "IBM Corporation"])
+    assert file.concluded_license == LicenseExpression("(LGPL-2.0-only OR LicenseRef-2)")
+    TestCase().assertCountEqual(file.license_info_in_file,
+                                [LicenseExpression("GPL-2.0-only"), LicenseExpression("LicenseRef-2")])
+    assert file.license_comment == "The concluded license was taken from the package level that the file was included in."
+    assert file.attribution_texts == ["Some attribution text."]
+
+
+def test_parse_incomplete_file():
+    file_parser = FileParser()
+    file_dict = {
+        "SPDXID": "SPDXRef-File",
+        "fileName": "Incomplete File"
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        file_parser.parse_file(file_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(),
+                                ["Error while constructing File: ['SetterError File: type of argument "
+                                 '"checksums" must be a list; got NoneType instead: None\']'])
+
+
+def test_parse_invalid_files():
+    file_parser = FileParser()
+    files = [{"SPDXID": "SPDXRef-File",
+              "fileName": "Incomplete File"},
+             {"SPDXID": "SPDXRef-File",
+              "attributionTexts": ["Some attribution text."],
+              "checksums": [{
+                  "algorithm": "SHA1",
+                  "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2758"
+              }, {
+                  "algorithm": "MD5",
+                  "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+              }]},
+             {"SPDXID": "SPDXRef-File",
+              "attributionTexts": ["Some attribution text."],
+              "checksums": [{
+                  "algorithm": "SHA1",
+                  "checksumValue": "d6a770ba38583ed4bb4525bd96e50461655d2758"
+              }, {
+                  "algorithm": "MD",
+                  "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+              }]},
+             ]
+
+    with pytest.raises(SPDXParsingError) as err:
+        parse_list_of_elements(files, file_parser.parse_file)
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while constructing File: ['SetterError File: type of argument " '"checksums" must be a list; got NoneType instead: None\']',
+        'Error while constructing File: [\'SetterError File: type of argument "name" ' "must be str; got NoneType instead: None']",
+        'Error while parsing File: ["Error while parsing Checksum: [\'Invalid ChecksumAlgorithm: MD\']"]'])
+
+
+def test_parse_file_types():
+    file_parser = FileParser()
+    file_types_list = ["OTHER", "APPLICATION"]
+
+    file_types = file_parser.parse_file_types(file_types_list)
+
+    TestCase().assertCountEqual(file_types, [FileType.OTHER, FileType.APPLICATION])
+
+
+def test_parse_invalid_file_types():
+    file_parser = FileParser()
+    file_types_list = ["OTHER", "APPLICAON"]
+
+    with pytest.raises(SPDXParsingError) as err:
+        file_parser.parse_file_types(file_types_list)
+
+    TestCase().assertCountEqual(err.value.get_messages(), ["Error while parsing FileType: ['Invalid FileType: APPLICAON']"])
diff --git a/tests/parser/test_json_parser.py b/tests/parser/test_json_parser.py
new file mode 100644
index 0000000..3b6ef31
--- /dev/null
+++ b/tests/parser/test_json_parser.py
@@ -0,0 +1,55 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import pytest
+
+from src.model.document import Document
+from src.parser.error import SPDXParsingError
+from src.parser.json.json_parser import JsonParser
+
+def test_parse_json_file_not_found():
+    with pytest.raises(FileNotFoundError) as err:
+        wrong_file_path = os.path.join(os.path.dirname(__file__), 'hnjfkjsedhnflsiafg.json')
+        JsonParser().parse(wrong_file_path)
+
+    assert err.value.args[1] == "No such file or directory"
+
+
+def test_parse_json_with_2_3_example():
+    doc = JsonParser().parse(os.path.join(os.path.dirname(__file__),"../data/formats/SPDXJSONExample-v2.3.spdx.json"))
+    assert type(doc) == Document
+    assert len(doc.annotations) == 5
+    assert len(doc.files) == 5
+    assert len(doc.packages) == 4
+    assert len(doc.snippets) ==  1
+    assert len(doc.relationships) == 23
+    assert len(doc.extracted_licensing_info) == 5
+
+def test_parse_json_with_2_2_example():
+    doc = JsonParser().parse(os.path.join(os.path.dirname(__file__),"../data/formats/SPDXJSONExample-v2.2.spdx.json"))
+    assert type(doc) == Document
+    assert len(doc.annotations) == 5
+    assert len(doc.files) == 4
+    assert len(doc.packages) == 4
+    assert len(doc.snippets) ==  1
+    assert len(doc.relationships) == 11
+    assert len(doc.extracted_licensing_info) == 5
+
+def test_parse_json_with_2_1_example():
+    doc = JsonParser().parse(os.path.join(os.path.dirname(__file__),"../data/formats/SPDXJsonExample.json"))
+    assert type(doc) == Document
+    assert len(doc.annotations) == 1
+    assert len(doc.files) == 2
+    assert len(doc.packages) == 1
+    assert len(doc.snippets) ==  1
+    assert len(doc.relationships) == 3
+    assert len(doc.extracted_licensing_info) == 4
diff --git a/tests/parser/test_license_expression_parser.py b/tests/parser/test_license_expression_parser.py
new file mode 100644
index 0000000..89e23a5
--- /dev/null
+++ b/tests/parser/test_license_expression_parser.py
@@ -0,0 +1,57 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest import TestCase
+
+import pytest
+
+from src.model.license_expression import LicenseExpression
+from src.parser.error import SPDXParsingError
+from src.parser.json.license_expression_parser import LicenseExpressionParser
+
+
+@pytest.mark.parametrize("invalid_license_expression,expected_message",
+                         [(56, ["Error while constructing LicenseExpression: ['SetterError LicenseExpression: "
+                                'type of argument "expression_string" must be str; got int instead: 56\']']
+                           ), ])
+def test_parse_invalid_license_expression(invalid_license_expression, expected_message):
+    license_expression_parser = LicenseExpressionParser()
+
+    with pytest.raises(SPDXParsingError) as err:
+        license_expression_parser.parse_license_expression(invalid_license_expression)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
+
+
+def test_parse_license_expressions():
+    license_expression_parser = LicenseExpressionParser()
+    license_expressions_list = ["First License", "Second License", "Third License"]
+
+    license_expressions = license_expression_parser.parse_license_expressions(license_expressions_list)
+
+    assert len(license_expressions) == 3
+    TestCase().assertCountEqual(license_expressions,
+                                [LicenseExpression("First License"), LicenseExpression("Second License"),
+                                 LicenseExpression("Third License")])
+
+
+@pytest.mark.parametrize("invalid_license_expressions,expected_message", [(["First Expression", 4, 6],
+                                                                           [
+                                                                               "Error while constructing LicenseExpression: ['SetterError LicenseExpression: "
+                                                                               'type of argument "expression_string" must be str; got int instead: 4\']',
+                                                                               "Error while constructing LicenseExpression: ['SetterError LicenseExpression: "
+                                                                               'type of argument "expression_string" must be str; got int instead: 6\']'])])
+def test_parse_invalid_license_expressions(invalid_license_expressions, expected_message):
+    license_expression_parser = LicenseExpressionParser()
+
+    with pytest.raises(SPDXParsingError) as err:
+        license_expression_parser.parse_license_expressions(invalid_license_expressions)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
diff --git a/tests/parser/test_package_parser.py b/tests/parser/test_package_parser.py
new file mode 100644
index 0000000..d5b9d9e
--- /dev/null
+++ b/tests/parser/test_package_parser.py
@@ -0,0 +1,211 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from datetime import datetime
+from unittest import TestCase
+
+import pytest
+
+from src.model.actor import Actor, ActorType
+from src.model.checksum import Checksum, ChecksumAlgorithm
+from src.model.license_expression import LicenseExpression
+from src.model.package import PackageVerificationCode, ExternalPackageRef, ExternalPackageRefCategory, PackagePurpose
+from src.parser.error import SPDXParsingError
+from src.parser.json.dict_parsing_functions import parse_list_of_elements
+from src.parser.json.package_parser import PackageParser
+
+
+def test_parse_package():
+    package_parser = PackageParser()
+
+    package_dict = {
+        "SPDXID": "SPDXRef-Package",
+        "attributionTexts": [
+            "The GNU C Library is free software.  See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed.  License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually."],
+        "builtDate": "2011-01-29T18:30:22Z",
+        "checksums": [{
+            "algorithm": "MD5",
+            "checksumValue": "624c1abb3664f4b35547e7c73864ad24"
+        }, {
+            "algorithm": "SHA1",
+            "checksumValue": "85ed0817af83a24ad8da68c2b5094de69833983c"
+        }, {
+            "algorithm": "SHA256",
+            "checksumValue": "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"
+        }, {
+            "algorithm": "BLAKE2b-384",
+            "checksumValue": "aaabd89c926ab525c242e6621f2f5fa73aa4afe3d9e24aed727faaadd6af38b620bdb623dd2b4788b1c8086984af8706"
+        }],
+        "comment": "This is a comment.",
+        "copyrightText": "Copyright 2008-2010 John Smith",
+        "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",
+        "externalRefs": [{
+            "referenceCategory": "SECURITY",
+            "referenceLocator": "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*",
+            "referenceType": "cpe23Type"
+        }, {
+            "comment": "This is the external ref for Acme",
+            "referenceCategory": "OTHER",
+            "referenceLocator": "acmecorp/acmenator/4.1.3-alpha",
+            "referenceType": "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge"
+        }],
+        "filesAnalyzed": True,
+        "homepage": "http://ftp.gnu.org/gnu/glibc",
+        "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"],
+        "name": "glibc",
+        "originator": "Organization: ExampleCodeInspect (contact@example.com)",
+        "packageFileName": "glibc-2.11.1.tar.gz",
+        "packageVerificationCode": {
+            "packageVerificationCodeExcludedFiles": ["./package.spdx"],
+            "packageVerificationCodeValue": "d6a770ba38583ed4bb4525bd96e50461655d2758"
+        },
+        "primaryPackagePurpose": "SOURCE",
+        "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)",
+        "validUntilDate": "2014-01-29T18:30:22Z",
+        "versionInfo": "2.11.1"
+    }
+
+    package = package_parser.parse_package(package_dict)
+
+    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.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.files_analyzed == True
+    assert package.verification_code == PackageVerificationCode(value="d6a770ba38583ed4bb4525bd96e50461655d2758",
+                                                                excluded_files=["./package.spdx"])
+    assert len(package.checksums) == 4
+    TestCase().assertCountEqual(package.checksums, [Checksum(ChecksumAlgorithm.MD5, "624c1abb3664f4b35547e7c73864ad24"),
+                                                    Checksum(ChecksumAlgorithm.SHA1,
+                                                             "85ed0817af83a24ad8da68c2b5094de69833983c"),
+                                                    Checksum(ChecksumAlgorithm.SHA256,
+                                                             "11b6d3ee554eedf79299905a98f9b9a04e498210b59f15094c916c91d150efcd"),
+                                                    Checksum(ChecksumAlgorithm.BLAKE2B_384,
+                                                             "aaabd89c926ab525c242e6621f2f5fa73aa4afe3d9e24aed727faaadd6af38b620bdb623dd2b4788b1c8086984af8706")])
+    assert package.homepage == "http://ftp.gnu.org/gnu/glibc"
+    assert package.source_info == "uses glibc-2_11-branch from git://sourceware.org/git/glibc.git."
+    assert package.license_concluded == LicenseExpression("(LGPL-2.0-only OR LicenseRef-3)")
+    TestCase().assertCountEqual(package.license_info_from_files,
+                                [LicenseExpression("GPL-2.0-only"), LicenseExpression("LicenseRef-2"),
+                                 LicenseExpression("LicenseRef-1")])
+    assert package.license_declared == LicenseExpression("(LGPL-2.0-only AND LicenseRef-3)")
+    assert package.license_comment == "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.summary == "GNU C library."
+    assert package.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."
+    assert package.comment == "This is a comment."
+    assert len(package.external_references) == 2
+    TestCase().assertCountEqual(package.external_references, [ExternalPackageRef(ExternalPackageRefCategory.SECURITY,
+                                                                                 "cpe23Type",
+                                                                                 "cpe:2.3:a:pivotal_software:spring_framework:4.1.0:*:*:*:*:*:*:*"),
+                                                              ExternalPackageRef(ExternalPackageRefCategory.OTHER,
+                                                                                 "http://spdx.org/spdxdocs/spdx-example-444504E0-4F89-41D3-9A0C-0305E82C3301#LocationRef-acmeforge",
+                                                                                 locator="acmecorp/acmenator/4.1.3-alpha",
+                                                                                 comment="This is the external ref for Acme")])
+    assert package.attribution_texts == [
+        "The GNU C Library is free software.  See the file COPYING.LIB for copying conditions, and LICENSES for notices about a few contributions that require these additional notices to be distributed.  License copyright years may be listed using range notation, e.g., 1996-2015, indicating that every year in the range, inclusive, is a copyrightable year that would otherwise be listed individually."]
+    assert package.primary_package_purpose == PackagePurpose.SOURCE
+    assert package.release_date == datetime(2012, 1, 29, 18, 30, 22)
+    assert package.built_date == datetime(2011, 1, 29, 18, 30, 22)
+    assert package.valid_until_date == datetime(2014, 1, 29, 18, 30, 22)
+
+
+@pytest.mark.parametrize("incomplete_package_dict,expected_message", [({"SPDXID": "SPDXRef-Package"}, [
+    "Error while constructing Package: ['SetterError Package: type of " 'argument "name" must be str; got NoneType instead: None\', \'SetterError Package: type of argument "download_location" must be one of (str, src.model.spdx_no_assertion.SpdxNoAssertion, src.model.spdx_none.SpdxNone); ' "got NoneType instead: None']"]),
+                                                                      ({"SPDXID": "SPDXRef-Package", "name": 5,
+                                                                        "downloadLocation": "NONE"}, [
+                                                                           "Error while constructing Package: ['SetterError Package: type of argument " '"name" must be str; got int instead: 5\']'])])
+def test_parse_incomplete_package(incomplete_package_dict, expected_message):
+    package_parser = PackageParser()
+
+    with pytest.raises(SPDXParsingError) as err:
+        package_parser.parse_package(incomplete_package_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), expected_message)
+
+
+def test_parse_invalid_package():
+    package_parser = PackageParser()
+    package_dict = {
+        "SPDXID": "SPDXRef-Package",
+        "name": "Example Package",
+        "downloadLocation": "NONE",
+        "checksums":
+            [{"algorithm": "SHA",
+              "value": "1234"}]
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        package_parser.parse_package(package_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        'Error while parsing Package: ["Error while parsing Checksum: [\'Invalid ChecksumAlgorithm: SHA\']"]'])
+
+
+def test_parse_packages():
+    package_parser = PackageParser()
+    packages_list = [{
+        "SPDXID": "SPDXRef-Package",
+        "name": "Example Package",
+        "downloadLocation": "NONE",
+        "checksums":
+            [{"algorithm": "SHA",
+              "value": "1234"}]
+    }, {
+        "SPDXID": "SPDXRef-Package",
+        "name": 5,
+        "downloadLocation": "NONE"
+    },
+        {
+            "SPDXID": "SPDXRef-Package",
+            "name": "Example Package",
+            "downloadLocation": "NONE"}
+    ]
+
+    with pytest.raises(SPDXParsingError) as err:
+        parse_list_of_elements(packages_list, package_parser.parse_package)
+
+    TestCase().assertCountEqual(err.value.get_messages(),
+                                ['Error while parsing Package: ["Error while parsing Checksum: '
+                                 '[\'Invalid ChecksumAlgorithm: SHA\']"]',
+                                 "Error while constructing Package: ['SetterError Package: type of argument "
+                                 '"name" must be str; got int instead: 5\']'])
+
+
+def test_parse_external_ref():
+    package_parser = PackageParser()
+    external_ref = {
+        "referenceType": "fix"
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        package_parser.parse_external_ref(external_ref)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while constructing ExternalPackageRef: ['SetterError " 'ExternalPackageRef: type of argument "category" must be ' "src.model.package.ExternalPackageRefCategory; got NoneType instead: None', " '\'SetterError ExternalPackageRef: type of argument "locator" must be str; ' "got NoneType instead: None']"])
+
+def test_parse_invalid_external_package_ref_category():
+    package_parser = PackageParser()
+    external_package_ref_category = "TEST"
+
+    with pytest.raises(SPDXParsingError) as err:
+        package_parser.parse_external_ref_category(external_package_ref_category)
+
+    TestCase().assertCountEqual(err.value.get_messages(), ["Invalid ExternalPackageRefCategory: TEST"])
diff --git a/tests/parser/test_relationship_parser.py b/tests/parser/test_relationship_parser.py
new file mode 100644
index 0000000..d1afa64
--- /dev/null
+++ b/tests/parser/test_relationship_parser.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest import TestCase
+
+import pytest
+
+from src.model.relationship import RelationshipType, Relationship
+from src.parser.error import SPDXParsingError
+from src.parser.json.relationship_parser import RelationshipParser
+
+
+def test_parse_relationship():
+    relationship_parser = RelationshipParser()
+
+    relationship_dict = {
+        "spdxElementId": "SPDXRef-DOCUMENT",
+        "relationshipType": "CONTAINS",
+        "relatedSpdxElement": "SPDXRef-Package",
+        "comment": "Comment."
+    }
+
+    relationship = relationship_parser.parse_relationship(relationship_dict)
+
+    assert relationship.relationship_type == RelationshipType.CONTAINS
+    assert relationship.spdx_element_id == "SPDXRef-DOCUMENT"
+    assert relationship.related_spdx_element_id == "SPDXRef-Package"
+    assert relationship.comment == "Comment."
+
+
+def test_parse_incomplete_relationship():
+    relationship_parser = RelationshipParser()
+    relationship_dict = {
+        "spdxElementId": "SPDXRef-DOCUMENT",
+        "relatedSpdxElement": "SPDXRef-Package",
+        "comment": "Comment."
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        relationship_parser.parse_relationship(relationship_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while constructing Relationship: ['SetterError Relationship: type of " 'argument "relationship_type" must be ' "src.model.relationship.RelationshipType; got NoneType instead: None']"])
+
+
+def test_parse_relationship_type():
+    relationship_parser = RelationshipParser()
+    relationship_type_str = "DEPENDENCY_OF"
+
+    relationship_type = relationship_parser.parse_relationship_type(relationship_type_str)
+    assert relationship_type == RelationshipType.DEPENDENCY_OF
+
+
+def test_parse_document_describes():
+    relationship_parser = RelationshipParser()
+
+    document_dict = {
+        "SPDXID": "SPDXRef-DOCUMENT",
+        "documentDescribes": ["SPDXRef-Package", "SPDXRef-File", "SPDXRef-Snippet"]
+    }
+
+    relationships = relationship_parser.parse_document_describes(doc_spdx_id="SPDXRef-DOCUMENT",
+                                                                 described_spdx_ids=document_dict.get(
+                                                                     "documentDescribes"),
+                                                                 existing_relationships=[])
+
+    assert len(relationships) == 3
+    TestCase().assertCountEqual(relationships,
+                                [Relationship("SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-Package"),
+                                 Relationship("SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-File"),
+                                 Relationship("SPDXRef-DOCUMENT", RelationshipType.DESCRIBES, "SPDXRef-Snippet")])
+
+
+def test_parse_document_describes_without_duplicating_relationships():
+    relationship_parser = RelationshipParser()
+    document_dict = {
+        "SPDXID": "SPDXRef-DOCUMENT",
+        "documentDescribes": ["SPDXRef-Package", "SPDXRef-File"],
+        "relationships": [{"spdxElementId": "SPDXRef-DOCUMENT", "relatedSpdxElement": "SPDXRef-Package",
+                           "relationshipType": "DESCRIBES",
+                           "comment": "This relationship has a comment."},
+                          {"spdxElementId": "SPDXRef-File", "relatedSpdxElement": "SPDXRef-DOCUMENT",
+                           "relationshipType": "DESCRIBED_BY", "comment": "This relationship has a comment."}
+                          ]}
+
+    relationships = relationship_parser.parse_all_relationships(document_dict)
+
+    assert len(relationships) == 2
+    TestCase().assertCountEqual(relationships, [
+        Relationship(related_spdx_element_id="SPDXRef-Package", relationship_type=RelationshipType.DESCRIBES,
+                     spdx_element_id="SPDXRef-DOCUMENT", comment="This relationship has a comment."),
+        Relationship(related_spdx_element_id="SPDXRef-DOCUMENT", relationship_type=RelationshipType.DESCRIBED_BY,
+                     spdx_element_id="SPDXRef-File", comment="This relationship has a comment.")])
+
+
+def test_parse_has_files():
+    relationship_parser = RelationshipParser()
+    document_dict = {
+        "packages":
+            [{
+                "SPDXID": "SPDXRef-Package",
+                "hasFiles": ["SPDXRef-File1", "SPDXRef-File2"]
+            }]
+    }
+
+    relationships = relationship_parser.parse_has_files(document_dict.get("packages"), existing_relationships=[])
+
+    assert len(relationships) == 2
+    TestCase().assertCountEqual(relationships, [
+        Relationship(spdx_element_id="SPDXRef-Package", relationship_type=RelationshipType.CONTAINS,
+                     related_spdx_element_id="SPDXRef-File1"),
+        Relationship(spdx_element_id="SPDXRef-Package", relationship_type=RelationshipType.CONTAINS,
+                     related_spdx_element_id="SPDXRef-File2")])
+
+
+def test_parse_has_files_without_duplicating_relationships():
+    relationship_parser = RelationshipParser()
+    document_dict = {
+        "packages":
+            [{
+                "SPDXID": "SPDXRef-Package",
+                "hasFiles": ["SPDXRef-File1", "SPDXRef-File2"]
+            }]
+    }
+    existing_relationships = [
+        Relationship(spdx_element_id="SPDXRef-Package", relationship_type=RelationshipType.CONTAINS,
+                     related_spdx_element_id="SPDXRef-File1", comment="This relationship has a comment."),
+        Relationship(spdx_element_id="SPDXRef-File2", relationship_type=RelationshipType.CONTAINED_BY,
+                     related_spdx_element_id="SPDXRef-Package")]
+
+    relationships = relationship_parser.parse_has_files(document_dict.get("packages"),
+                                                        existing_relationships=existing_relationships)
+
+    assert len(relationships) == 0
diff --git a/tests/parser/test_snippet_parser.py b/tests/parser/test_snippet_parser.py
new file mode 100644
index 0000000..96e1bae
--- /dev/null
+++ b/tests/parser/test_snippet_parser.py
@@ -0,0 +1,141 @@
+# Copyright (c) 2022 spdx contributors
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#     http://www.apache.org/licenses/LICENSE-2.0
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+from unittest import TestCase
+
+import pytest
+
+from src.model.license_expression import LicenseExpression
+from src.parser.error import SPDXParsingError
+from src.parser.json.snippet_parser import SnippetParser
+
+
+def test_parse_snippet():
+    snippet_parser = SnippetParser()
+
+    snippet_dict = {
+        "SPDXID": "SPDXRef-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",
+        "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.",
+        "licenseConcluded": "GPL-2.0-only",
+        "licenseInfoInSnippets": ["GPL-2.0-only"],
+        "name": "from linux kernel",
+        "ranges": [{
+            "endPointer": {
+                "offset": 420,
+                "reference": "SPDXRef-DoapSource"
+            },
+            "startPointer": {
+                "offset": 310,
+                "reference": "SPDXRef-DoapSource"
+            }
+        }, {
+            "endPointer": {
+                "lineNumber": 23,
+                "reference": "SPDXRef-DoapSource"
+            },
+            "startPointer": {
+                "lineNumber": 5,
+                "reference": "SPDXRef-DoapSource"
+            }
+        }],
+        "snippetFromFile": "SPDXRef-DoapSource",
+        "attributionTexts": ["Some example attibution text."]
+    }
+    snippet = snippet_parser.parse_snippet(snippet_dict)
+
+    assert snippet.spdx_id == "SPDXRef-Snippet"
+    assert snippet.name == "from linux kernel"
+    assert 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."
+    assert snippet.copyright_text == "Copyright 2008-2010 John Smith"
+    assert snippet.license_comment == "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."
+    assert snippet.byte_range == (310, 420)
+    assert snippet.line_range == (5, 23)
+    assert snippet.file_spdx_id == "SPDXRef-DoapSource"
+    assert snippet.license_info_in_snippet == [LicenseExpression("GPL-2.0-only")]
+    assert snippet.concluded_license == LicenseExpression("GPL-2.0-only")
+    assert snippet.attribution_texts == ["Some example attibution text."]
+
+
+def test_parse_incomplete_snippet():
+    snippet_parser = SnippetParser()
+    incomplete_snippet_dict = {
+        "SPDXID": "SPDXRef-Snippet",
+        "file_spdx_id": "SPDXRef-File"
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        snippet_parser.parse_snippet(incomplete_snippet_dict)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while constructing Snippet: ['SetterError Snippet: type of argument " '"file_spdx_id" must be str; got NoneType instead: None\', \'SetterError Snippet: type of argument "byte_range" must be a tuple; got NoneType '
+        "instead: None']"])
+
+
+def test_parse_snippet_with_invalid_snippet_range():
+    snippet_parser = SnippetParser()
+    snippet_with_invalid_ranges_list = {
+        "SPDXID": "SPDXRef-Snippet",
+        "file_spdx_id": "SPDXRef-File",
+        "ranges": [
+            {
+                "endPointer": {
+                    "offset": 23,
+                    "reference": "SPDXRef-DoapSource"
+                },
+                "startPointer": {
+                    "offset": "310",
+                    "reference": "SPDXRef-DoapSource"
+                }
+            }]
+    }
+
+    with pytest.raises(SPDXParsingError) as err:
+        snippet_parser.parse_snippet(snippet_with_invalid_ranges_list)
+
+    TestCase().assertCountEqual(err.value.get_messages(),
+                                ["Error while constructing Snippet: ['SetterError Snippet: type of argument "
+                                 '"file_spdx_id" must be str; got NoneType instead: None\', \'SetterError '
+                                 'Snippet: type of argument "byte_range"[0] must be int; got str instead: '
+                                 "(\\'310\\', 23)']"])
+
+
+def test_parse_invalid_snippet_range():
+    snippet_parser = SnippetParser()
+
+    ranges = [
+        {
+            "endPointer": {
+                "lineNumber": 23,
+                "reference": "SPDXRef-DoapSource"
+            },
+            "startPointer": {
+                "offset": 310,
+                "reference": "SPDXRef-DoapSource"
+            }
+        }, {
+            "endPointer": {
+                "offset": 420,
+                "reference": "SPDXRef-DoapSource"
+            },
+            "startPointer": {
+                "lineNumber": 5,
+                "reference": "SPDXRef-DoapSource"
+            }
+        }
+
+    ]
+
+    with pytest.raises(SPDXParsingError) as err:
+        snippet_parser.parse_ranges(ranges)
+
+    TestCase().assertCountEqual(err.value.get_messages(), [
+        "Error while parsing snippet ranges: ['Type of startpointer is not the same as type of endpointer.', 'Type of startpointer is not the same as type of endpointer.']"])