Repository: bo4e/BO4E-python (language: python)

Issue:
Replace jsons with marshmallow to realise the serialisation
jsons did a great job at the beginning, but failed in the interaction with sqlalchemy.
Therefor we will use marshmallow for the serialisation.


Discussion / hints:

Fix patch:
diff --git a/.github/workflows/black.yml b/.github/workflows/black.yml
new file mode 100644
index 000000000..12235d39f
--- /dev/null
+++ b/.github/workflows/black.yml
@@ -0,0 +1,16 @@
+name: "Black"
+
+on: [push]
+jobs:
+  black:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        python-version: [3.9]
+        os: [ubuntu-latest]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Black Code Formatter
+        uses: lgeiger/black-action@v1.0.1
+        with:
+          args: ". --check"
diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
new file mode 100644
index 000000000..296327629
--- /dev/null
+++ b/.github/workflows/codeql-analysis.yml
@@ -0,0 +1,71 @@
+# For most projects, this workflow file will not need changing; you simply need
+# to commit it to your repository.
+#
+# You may wish to alter this file to override the set of languages analyzed,
+# or to provide custom queries or build logic.
+#
+# ******** NOTE ********
+# We have attempted to detect the languages in your repository. Please check
+# the `language` matrix defined below to confirm you have the correct set of
+# supported CodeQL languages.
+#
+name: "CodeQL"
+
+on:
+  push:
+    branches: [ develop, master ]
+  pull_request:
+    # The branches below must be a subset of the branches above
+    branches: [ develop ]
+  schedule:
+    - cron: '20 18 * * 4'
+
+jobs:
+  analyze:
+    name: Analyze
+    runs-on: ubuntu-latest
+    permissions:
+      actions: read
+      contents: read
+      security-events: write
+
+    strategy:
+      fail-fast: false
+      matrix:
+        language: [ 'python' ]
+        # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ]
+        # Learn more:
+        # https://docs.github.com/en/free-pro-team@latest/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#changing-the-languages-that-are-analyzed
+
+    steps:
+    - name: Checkout repository
+      uses: actions/checkout@v2
+
+    # Initializes the CodeQL tools for scanning.
+    - name: Initialize CodeQL
+      uses: github/codeql-action/init@v1
+      with:
+        languages: ${{ matrix.language }}
+        # If you wish to specify custom queries, you can do so here or in a config file.
+        # By default, queries listed here will override any specified in a config file.
+        # Prefix the list here with "+" to use these queries and those in the config file.
+        # queries: ./path/to/local/query, your-org/your-repo/queries@main
+
+    # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
+    # If this step fails, then you should remove it and run the build manually (see below)
+    - name: Autobuild
+      uses: github/codeql-action/autobuild@v1
+
+    # ℹ️ Command-line programs to run using the OS shell.
+    # 📚 https://git.io/JvXDl
+
+    # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines
+    #    and modify them (or add more) to build your code if your project
+    #    uses a compiled language
+
+    #- run: |
+    #   make bootstrap
+    #   make release
+
+    - name: Perform CodeQL Analysis
+      uses: github/codeql-action/analyze@v1
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
new file mode 100644
index 000000000..76f07cbb9
--- /dev/null
+++ b/.github/workflows/coverage.yml
@@ -0,0 +1,23 @@
+name: "Coverage"
+
+on: [push]
+jobs:
+  coverage:
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        python-version: [3.9]
+        os: [ubuntu-latest]
+    steps:
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install tox
+      - name: Run Tests and Record Coverage
+        run: |
+          tox -e coverage
diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml
index d2ea13dde..3a64a265c 100644
--- a/.github/workflows/linting.yml
+++ b/.github/workflows/linting.yml
@@ -1,23 +1,23 @@
 name: "Linting"
-on: [push]
+on: [ push ]
 jobs:
   linting:
     name: Python Code Quality and Lint
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: [3.8]
-        os: [ubuntu-latest]
+        python-version: [ 3.8 ]
+        os: [ ubuntu-latest ]
     steps:
-    - uses: actions/checkout@v2
-    - name: Set up Python ${{ matrix.python-version }}
-      uses: actions/setup-python@v2
-      with:
-        python-version: ${{ matrix.python-version }}
-    - name: Install dependencies
-      run: |
-        python -m pip install --upgrade pip
-        pip install tox
-    - name: Linting bo4e with flake8 environment
-      run: |
-        tox -e flake8
\ No newline at end of file
+      - uses: actions/checkout@v2
+      - name: Set up Python ${{ matrix.python-version }}
+        uses: actions/setup-python@v2
+        with:
+          python-version: ${{ matrix.python-version }}
+      - name: Install dependencies
+        run: |
+          python -m pip install --upgrade pip
+          pip install tox
+      - name: Linting bo4e
+        run: |
+          tox -e linting
diff --git a/.github/workflows/python-publish.yml b/.github/workflows/python-publish.yml
index e1a020283..7ce56dd36 100644
--- a/.github/workflows/python-publish.yml
+++ b/.github/workflows/python-publish.yml
@@ -6,15 +6,15 @@ name: Upload Python Package
 on:
   release:
     branches: master
-    types: [created, edited]
+    types: [ created, edited ]
 
 jobs:
   tests:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: [3.8]
-        os: [ubuntu-latest]
+        python-version: [ 3.8 ]
+        os: [ ubuntu-latest ]
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
new file mode 100644
index 000000000..6ef9fdbbd
--- /dev/null
+++ b/.pre-commit-config.yaml
@@ -0,0 +1,13 @@
+exclude: azure-pipelines.yml
+repos:
+  - repo: https://github.com/pre-commit/pre-commit-hooks
+    rev: v2.3.0
+    hooks:
+      - id: check-yaml
+      - id: end-of-file-fixer
+      - id: trailing-whitespace
+  - repo: https://github.com/psf/black
+    rev: 21.5b2 # Replace by any tag/version: https://github.com/psf/black/tags
+    hooks:
+      - id: black
+        language_version: python3 # Should be a command that runs python3.6+
diff --git a/.pylintrc b/.pylintrc
new file mode 100644
index 000000000..8b83e4a3d
--- /dev/null
+++ b/.pylintrc
@@ -0,0 +1,611 @@
+[MASTER]
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code.
+extension-pkg-allow-list=
+
+# A comma-separated list of package or module names from where C extensions may
+# be loaded. Extensions are loading into the active Python interpreter and may
+# run arbitrary code. (This is an alternative name to extension-pkg-allow-list
+# for backward compatibility.)
+extension-pkg-whitelist=
+
+# Specify a score threshold to be exceeded before program exits with error.
+fail-under=10.0
+
+# Files or directories to be skipped. They should be base names, not paths.
+ignore=CVS
+
+# Files or directories matching the regex patterns are skipped. The regex
+# matches against base names, not paths.
+ignore-patterns=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+init-hook='import sys; sys.path.append(".")'
+
+# Use multiple processes to speed up Pylint. Specifying 0 will auto-detect the
+# number of processors available to use.
+jobs=1
+
+# Control the amount of potential inferred values when inferring a single
+# object. This can help the performance when dealing with large functions or
+# complex, nested conditions.
+limit-inference-results=100
+
+# List of plugins (as comma separated values of python module names) to load,
+# usually to register additional checkers.
+#load-plugins=
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# When enabled, pylint would attempt to guess common misconfiguration and emit
+# user-friendly hints instead of false-positive error messages.
+suggestion-mode=yes
+
+# Allow loading of arbitrary C extensions. Extensions are imported into the
+# active Python interpreter and may run arbitrary code.
+unsafe-load-any-extension=no
+
+
+[MESSAGES CONTROL]
+
+# Only show warnings with the listed confidence levels. Leave empty to show
+# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED.
+confidence=
+
+# Disable the message, report, category or checker with the given id(s). You
+# can either give multiple identifiers separated by comma (,) or put this
+# option multiple times (only on the command line, not in the configuration
+# file where it should appear only once). You can also use "--disable=all" to
+# disable everything first and then reenable specific checks. For example, if
+# you want to run only the similarities checker, you can use "--disable=all
+# --enable=similarities". If you want to run only the classes checker, but have
+# no Warning level messages displayed, use "--disable=all --enable=classes
+# --disable=W".
+disable=print-statement,
+        parameter-unpacking,
+        unpacking-in-except,
+        old-raise-syntax,
+        backtick,
+        long-suffix,
+        old-ne-operator,
+        old-octal-literal,
+        import-star-module-level,
+        non-ascii-bytes-literal,
+        raw-checker-failed,
+        bad-inline-option,
+        locally-disabled,
+        file-ignored,
+        suppressed-message,
+        useless-suppression,
+        deprecated-pragma,
+        use-symbolic-message-instead,
+        apply-builtin,
+        basestring-builtin,
+        buffer-builtin,
+        cmp-builtin,
+        coerce-builtin,
+        execfile-builtin,
+        file-builtin,
+        long-builtin,
+        raw_input-builtin,
+        reduce-builtin,
+        standarderror-builtin,
+        unicode-builtin,
+        xrange-builtin,
+        coerce-method,
+        delslice-method,
+        getslice-method,
+        setslice-method,
+        no-absolute-import,
+        old-division,
+        dict-iter-method,
+        dict-view-method,
+        next-method-called,
+        metaclass-assignment,
+        indexing-exception,
+        raising-string,
+        reload-builtin,
+        oct-method,
+        hex-method,
+        nonzero-method,
+        cmp-method,
+        input-builtin,
+        round-builtin,
+        intern-builtin,
+        unichr-builtin,
+        map-builtin-not-iterating,
+        zip-builtin-not-iterating,
+        range-builtin-not-iterating,
+        filter-builtin-not-iterating,
+        using-cmp-argument,
+        eq-without-hash,
+        div-method,
+        idiv-method,
+        rdiv-method,
+        exception-message-attribute,
+        invalid-str-codec,
+        sys-max-int,
+        bad-python3-import,
+        deprecated-string-function,
+        deprecated-str-translate-call,
+        deprecated-itertools-function,
+        deprecated-types-field,
+        next-method-defined,
+        dict-items-not-iterating,
+        dict-keys-not-iterating,
+        dict-values-not-iterating,
+        deprecated-operator-function,
+        deprecated-urllib-function,
+        xreadlines-attribute,
+        deprecated-sys-function,
+        exception-escape,
+        comprehension-escape
+
+# Enable the message, report, category or checker with the given id(s). You can
+# either give multiple identifier separated by comma (,) or put this option
+# multiple time (only on the command line, not in the configuration file where
+# it should appear only once). See also the "--disable" option for examples.
+enable=c-extension-no-member
+
+
+[REPORTS]
+
+# Python expression which should return a score less than or equal to 10. You
+# have access to the variables 'error', 'warning', 'refactor', and 'convention'
+# which contain the number of messages in each category, as well as 'statement'
+# which is the total number of statements analyzed. This score is used by the
+# global evaluation report (RP0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Template used to display messages. This is a python new-style format string
+# used to format the message information. See doc for all details.
+#msg-template=
+
+# Set the output format. Available formats are text, parseable, colorized, json
+# and msvs (visual studio). You can also give a reporter class, e.g.
+# mypackage.mymodule.MyReporterClass.
+output-format=text
+
+# Tells whether to display a full report or only the messages.
+reports=no
+
+# Activate the evaluation score.
+score=yes
+
+
+[REFACTORING]
+
+# Maximum number of nested blocks for function / method body
+max-nested-blocks=5
+
+# Complete name of functions that never returns. When checking for
+# inconsistent-return-statements if a never returning function is called then
+# it will be considered as an explicit return statement and no message will be
+# printed.
+never-returning-functions=sys.exit
+
+
+[BASIC]
+
+# Naming style matching correct argument names.
+argument-naming-style=snake_case
+
+# Regular expression matching correct argument names. Overrides argument-
+# naming-style.
+#argument-rgx=
+
+# Naming style matching correct attribute names.
+attr-naming-style=snake_case
+
+# Regular expression matching correct attribute names. Overrides attr-naming-
+# style.
+#attr-rgx=
+
+# Bad variable names which should always be refused, separated by a comma.
+bad-names=foo,
+          bar,
+          baz,
+          toto,
+          tutu,
+          tata
+
+# Bad variable names regexes, separated by a comma. If names match any regex,
+# they will always be refused
+bad-names-rgxs=
+
+# Naming style matching correct class attribute names.
+class-attribute-naming-style=any
+
+# Regular expression matching correct class attribute names. Overrides class-
+# attribute-naming-style.
+#class-attribute-rgx=
+
+# Naming style matching correct class constant names.
+class-const-naming-style=UPPER_CASE
+
+# Regular expression matching correct class constant names. Overrides class-
+# const-naming-style.
+#class-const-rgx=
+
+# Naming style matching correct class names.
+class-naming-style=PascalCase
+
+# Regular expression matching correct class names. Overrides class-naming-
+# style.
+#class-rgx=
+
+# Naming style matching correct constant names.
+const-naming-style=UPPER_CASE
+
+# Regular expression matching correct constant names. Overrides const-naming-
+# style.
+#const-rgx=
+
+# Minimum line length for functions/classes that require docstrings, shorter
+# ones are exempt.
+docstring-min-length=-1
+
+# Naming style matching correct function names.
+function-naming-style=snake_case
+
+# Regular expression matching correct function names. Overrides function-
+# naming-style.
+#function-rgx=
+
+# Good variable names which should always be accepted, separated by a comma.
+good-names=i,
+           j,
+           k,
+           ex,
+           Run,
+           _
+
+# Good variable names regexes, separated by a comma. If names match any regex,
+# they will always be accepted
+good-names-rgxs=
+
+# Include a hint for the correct naming format with invalid-name.
+include-naming-hint=no
+
+# Naming style matching correct inline iteration names.
+inlinevar-naming-style=any
+
+# Regular expression matching correct inline iteration names. Overrides
+# inlinevar-naming-style.
+#inlinevar-rgx=
+
+# Naming style matching correct method names.
+method-naming-style=snake_case
+
+# Regular expression matching correct method names. Overrides method-naming-
+# style.
+#method-rgx=
+
+# Naming style matching correct module names.
+module-naming-style=snake_case
+
+# Regular expression matching correct module names. Overrides module-naming-
+# style.
+#module-rgx=
+
+# Colon-delimited sets of names that determine each other's naming style when
+# the name regexes allow several styles.
+name-group=
+
+# Regular expression which should only match function or class names that do
+# not require a docstring.
+no-docstring-rgx=^_
+
+# List of decorators that produce properties, such as abc.abstractproperty. Add
+# to this list to register other decorators that produce valid properties.
+# These decorators are taken in consideration only for invalid-name.
+property-classes=abc.abstractproperty
+
+# Naming style matching correct variable names.
+variable-naming-style=snake_case
+
+# Regular expression matching correct variable names. Overrides variable-
+# naming-style.
+#variable-rgx=
+
+
+[FORMAT]
+
+# Expected format of line ending, e.g. empty (any line ending), LF or CRLF.
+expected-line-ending-format=
+
+# Regexp for a line that is allowed to be longer than the limit.
+ignore-long-lines=^\s*(# )?<?https?://\S+>?$
+
+# Number of spaces of indent required inside a hanging or continued line.
+indent-after-paren=4
+
+# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
+# tab).
+indent-string='    '
+
+# Maximum number of characters on a single line.
+max-line-length=120
+
+# Maximum number of lines in a module.
+max-module-lines=1000
+
+# Allow the body of a class to be on the same line as the declaration if body
+# contains single statement.
+single-line-class-stmt=no
+
+# Allow the body of an if to be on the same line as the test if there is no
+# else.
+single-line-if-stmt=no
+
+
+[LOGGING]
+
+# The type of string formatting that logging methods do. `old` means using %
+# formatting, `new` is for `{}` formatting.
+logging-format-style=old
+
+# Logging modules to check that the string format arguments are in logging
+# function parameter format.
+logging-modules=logging
+
+
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,
+      XXX,
+      #TODO
+
+# Regular expression of note tags to take in consideration.
+#notes-rgx=
+
+
+[SIMILARITIES]
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+# Ignore imports when computing similarities.
+ignore-imports=yes
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+
+[SPELLING]
+
+# Limits count of emitted suggestions for spelling mistakes.
+max-spelling-suggestions=4
+
+# Spelling dictionary name. Available dictionaries: none. To make it work,
+# install the 'python-enchant' package.
+spelling-dict=
+
+# List of comma separated words that should be considered directives if they
+# appear and the beginning of a comment and should not be checked.
+spelling-ignore-comment-directives=fmt: on,fmt: off,noqa:,noqa,nosec,isort:skip,mypy:
+
+# List of comma separated words that should not be checked.
+spelling-ignore-words=
+
+# A path to a file that contains the private dictionary; one word per line.
+spelling-private-dict-file=
+
+# Tells whether to store unknown words to the private dictionary (see the
+# --spelling-private-dict-file option) instead of raising a message.
+spelling-store-unknown-words=no
+
+
+[STRING]
+
+# This flag controls whether inconsistent-quotes generates a warning when the
+# character used as a quote delimiter is used inconsistently within a module.
+check-quote-consistency=no
+
+# This flag controls whether the implicit-str-concat should generate a warning
+# on implicit string concatenation in sequences defined over several lines.
+check-str-concat-over-line-jumps=no
+
+
+[TYPECHECK]
+
+# List of decorators that produce context managers, such as
+# contextlib.contextmanager. Add to this list to register other decorators that
+# produce valid context managers.
+contextmanager-decorators=contextlib.contextmanager
+
+# List of members which are set dynamically and missed by pylint inference
+# system, and so shouldn't trigger E1101 when accessed. Python regular
+# expressions are accepted.
+generated-members=
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# Tells whether to warn about missing members when the owner of the attribute
+# is inferred to be None.
+ignore-none=yes
+
+# This flag controls whether pylint should warn about no-member and similar
+# checks whenever an opaque object is returned when inferring. The inference
+# can return multiple potential results while evaluating a Python object, but
+# some branches might not be evaluated, which results in partial inference. In
+# that case, it might be useful to still emit no-member and other checks for
+# the rest of the inferred objects.
+ignore-on-opaque-inference=yes
+
+# List of class names for which member attributes should not be checked (useful
+# for classes with dynamically set attributes). This supports the use of
+# qualified names.
+ignored-classes=optparse.Values,thread._local,_thread._local
+
+# List of module names for which member attributes should not be checked
+# (useful for modules/projects where namespaces are manipulated during runtime
+# and thus existing member attributes cannot be deduced by static analysis). It
+# supports qualified module names, as well as Unix pattern matching.
+#ignored-modules=
+
+# Show a hint with possible names when a member name was not found. The aspect
+# of finding the hint is based on edit distance.
+missing-member-hint=yes
+
+# The minimum edit distance a name should have in order to be considered a
+# similar match for a missing member name.
+missing-member-hint-distance=1
+
+# The total number of similar names that should be taken in consideration when
+# showing a hint for a missing member.
+missing-member-max-choices=1
+
+# List of decorators that change the signature of a decorated function.
+signature-mutators=
+
+
+[VARIABLES]
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid defining new builtins when possible.
+additional-builtins=
+
+# Tells whether unused global variables should be treated as a violation.
+allow-global-unused-variables=yes
+
+# List of names allowed to shadow builtins
+allowed-redefined-builtins=
+
+# List of strings which can identify a callback function by name. A callback
+# name must start or end with one of those strings.
+callbacks=cb_,
+          _cb
+
+# A regular expression matching the name of dummy variables (i.e. expected to
+# not be used).
+dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_
+
+# Argument names that match this expression will be ignored. Default to name
+# with leading underscore.
+ignored-argument-names=_.*|^ignored_|^unused_
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# List of qualified module names which can have objects that can redefine
+# builtins.
+redefining-builtins-modules=six.moves,past.builtins,future.builtins,builtins,io
+
+
+[CLASSES]
+
+# Warn about protected attribute access inside special methods
+check-protected-access-in-special-methods=no
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,
+                      __new__,
+                      setUp,
+                      __post_init__
+
+# List of member names, which should be excluded from the protected access
+# warning.
+exclude-protected=_asdict,
+                  _fields,
+                  _replace,
+                  _source,
+                  _make
+
+# List of valid names for the first argument in a class method.
+valid-classmethod-first-arg=cls
+
+# List of valid names for the first argument in a metaclass class method.
+valid-metaclass-classmethod-first-arg=cls
+
+
+[DESIGN]
+
+# Maximum number of arguments for function / method.
+max-args=5
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=7
+
+# Maximum number of boolean expressions in an if statement (see R0916).
+max-bool-expr=5
+
+# Maximum number of branch for function / method body.
+max-branches=12
+
+# Maximum number of locals for function / method body.
+max-locals=15
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=20
+
+# Maximum number of return / yield for function / method body.
+max-returns=6
+
+# Maximum number of statements in function / method body.
+max-statements=50
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+
+[IMPORTS]
+
+# List of modules that can be imported at any level, not just the top level
+# one.
+allow-any-import-level=
+
+# Allow wildcard imports from modules that define __all__.
+allow-wildcard-with-all=no
+
+# Analyse import fallback blocks. This can be used to support both Python 2 and
+# 3 compatible code, which means that the block might have code that exists
+# only in one or another interpreter, leading to false positives when analysed.
+analyse-fallback-blocks=no
+
+# Deprecated modules which should not be used, separated by a comma.
+deprecated-modules=optparse,tkinter.tix
+
+# Output a graph (.gv or any supported image format) of external dependencies
+# to the given file (report RP0402 must not be disabled).
+ext-import-graph=
+
+# Output a graph (.gv or any supported image format) of all (i.e. internal and
+# external) dependencies to the given file (report RP0402 must not be
+# disabled).
+import-graph=
+
+# Output a graph (.gv or any supported image format) of internal dependencies
+# to the given file (report RP0402 must not be disabled).
+int-import-graph=
+
+# Force import order to recognize a module as part of the standard
+# compatibility libraries.
+known-standard-library=
+
+# Force import order to recognize a module as part of a third party library.
+known-third-party=enchant
+
+# Couples of modules and preferred modules, separated by a comma.
+preferred-modules=
+
+
+[EXCEPTIONS]
+
+# Exceptions that will emit a warning when being caught. Defaults to
+# "BaseException, Exception".
+overgeneral-exceptions=BaseException,
+                       Exception
diff --git a/.vscode/settings.json b/.vscode/settings.json
index a00cf7dd9..bc8b59503 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -7,5 +7,5 @@
     "python.testing.pytestArgs": [
         "tests"
     ],
-    "python.linting.flake8Enabled": true
+    "python.linting.pylintEnabled": true
 }
\ No newline at end of file
diff --git a/README.rst b/README.rst
index 8583bc388..3def9201c 100644
--- a/README.rst
+++ b/README.rst
@@ -23,8 +23,15 @@ BO4E
 
 
 
-Python Library that Implements the `BO4E Standard`_.
-Requires Python >=3.8. See also our `DOTNET implementation`_.
+Python Library that Implements `BO4E <https://www.bo4e.de/dokumentation>`_.
+Requires Python >=3.8.
+
+Other Noteworthy BO4E Implementations
+=====================================
+
+* `C#/.NET`_
+* `Golang`_
+* `Kotlin`_
 
 Contributing
 ============
@@ -36,14 +43,60 @@ To enhance this BO4E implementation and contribute to this project check out the
 
 .. code-block:: Shell
 
-   tox -e dev 
-   
+   tox -e dev
+
 The created venv should be located somewhere around .tox/dev/Scripts.
 
+Regular Expression for Enumerations
+-----------------------------------
+
+If you want to add a new enumeration from the `BO4E website`_ then you can use the following regular expression pattern:
+
+.. code-block:: Shell
+
+    ^(?<wert>[A-Z\d_]+)\t(?<bedeutung>.+)$
+
+In combination with this substitution:
+
+.. code-block:: Shell
+
+    "$wert": "$wert", # $bedeutung
+
+This substitution can directly used on the website `regex101`_.
+
+
 Versioning
 ==========
-To track the versions of this python package we use `setuptools-scm <https://pypi.org/project/setuptools-scm/>`_.
-There are different kind of ways to achieve the version tracking. We will use the `pyproject.toml` usage.
+| Short background information about versioning of python packages.
+| At the moment (2021-02-10) there are `seven ways to define the version of your package <https://packaging.python.org/guides/single-sourcing-package-version/>`_.
+| We use `setuptools-scm <https://pypi.org/project/setuptools-scm/>`_ for versioning so we can use the tags of git to define the version.
+| The tool itself again has several ways how to configure it.
+| We use the `pyproject.toml` file to configure setuptools-scm.
+| There we tell the build-system with ``"setuptools_scm[toml]>=3.4"`` that we use setuptools_scm and the version must be at least ``3.4``.
+| The ``[toml]`` section tells setuptools-scm that it finds all settings in our pyproject.toml file.
+| ``[tool.setuptools_scm]`` in pyproject.toml enables version inference.
+| In the setup.py we have to use the attribute ``use_scm_version=True``.
+
+To create the version number itself, we stick to the default behavior of setuptools-scm.
+It will take a look at three things:
+
+1. latest tag (with a version number)
+2. the distance to this tag (e.g. number of revisions since latest tag)
+3. workdir state (e.g. uncommitted changes since latest tag)
+
+and uses roughly the following logic to render the version:
+
+no distance and clean:
+    ``{tag}``
+distance and clean:
+    ``{next_version}.dev{distance}+{scm letter}{revision hash}``
+no distance and not clean:
+    ``{tag}+dYYYYMMDD``
+distance and not clean:
+    ``{next_version}.dev{distance}+{scm letter}{revision hash}.dYYYYMMDD``
+
+
+The next version is calculated by adding 1 to the last numeric component of the tag.
 
 To get the current version run in your working directory:
 
@@ -54,12 +107,14 @@ To get the current version run in your working directory:
 At the moment it is not possible to get the version number at runtime.
 To achieve this, we have to implement `Retrieving package version at runtime`_.
 
+If you follow the instruction in the *release workflow*, you will get the version number which you define with the label name.
+
 Release workflow
 ================
 * Check with tox all tests and lintings: `tox`
 * Check with tox if the packaging works fine: `tox -e test_packaging`
 * Merge all your changes you would like to have in the release into the master branch (`open new PR develop→master`_)
-* Check that all Github actions for tests and linting do pass (should be automatically enforced for PRs against master) 
+* Check that all Github actions for tests and linting do pass (should be automatically enforced for PRs against master)
 * Go to `BO4E-python`_ and click on "`Draft a new release`_" in the right sidebar
 * Write in the *Tag version* field and in the *Release title* your new version, i.e. `v0.0.6`
 * Add a describtion to the release
@@ -76,8 +131,10 @@ According to `Kununu ratings`_ Hochfrequenz is among the most attractive employe
 Applications of talented developers are welcome at any time! Please consider visiting our `career page`_ that also contains job openings.
 
 
-.. _`BO4E Standard`: https://www.bo4e.de/dokumentation
-.. _`DOTNET implementation`: https://github.com/Hochfrequenz/BO4E-dotnet
+.. _`BO4E website`: https://www.bo4e.de/dokumentation
+.. _`C#/.NET`: https://github.com/Hochfrequenz/BO4E-dotnet
+.. _`Golang`: https://github.com/Hochfrequenz/go-bo4e/
+.. _`Kotlin`: https://github.com/openEnWi/ktBO4E-lib
 .. _`Hochfrequenz Unternehmensberatung GmbH`: https://www.hochfrequenz.de
 .. _`Kununu ratings`: https://www.kununu.com/de/hochfrequenz-unternehmensberatung1
 .. _`career page`: https://www.hochfrequenz.de/karriere/stellenangebote/full-stack-entwickler/
@@ -87,3 +144,4 @@ Applications of talented developers are welcome at any time! Please consider vis
 .. _`open new PR develop→master`: https://github.com/Hochfrequenz/BO4E-python/compare/master...develop
 .. _`Draft a new release`: https://github.com/Hochfrequenz/BO4E-python/releases/new
 .. _`Retrieving package version at runtime`: https://pypi.org/project/setuptools-scm/
+.. _`regex101`: https://regex101.com/r/JWeb51/1
diff --git a/docs/_static/bo4e-python-favicon.svg b/docs/_static/bo4e-python-favicon.svg
new file mode 100644
index 000000000..8ea3cad22
--- /dev/null
+++ b/docs/_static/bo4e-python-favicon.svg
@@ -0,0 +1,96 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="54.663067mm"
+   height="54.378391mm"
+   viewBox="0 0 54.663067 54.378391"
+   version="1.1"
+   id="svg8"
+   sodipodi:docname="favicon.svg"
+   inkscape:version="1.0 (4035a4fb49, 2020-05-01)">
+  <defs
+     id="defs2" />
+  <sodipodi:namedview
+     fit-margin-bottom="0"
+     fit-margin-right="0"
+     fit-margin-left="0"
+     fit-margin-top="0"
+     id="base"
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1.0"
+     inkscape:pageopacity="0.0"
+     inkscape:pageshadow="2"
+     inkscape:zoom="0.98994949"
+     inkscape:cx="157.60476"
+     inkscape:cy="131.38142"
+     inkscape:document-units="mm"
+     inkscape:current-layer="layer1"
+     inkscape:document-rotation="0"
+     showgrid="true"
+     inkscape:snap-bbox="true"
+     inkscape:bbox-paths="true"
+     inkscape:bbox-nodes="true"
+     inkscape:snap-bbox-edge-midpoints="true"
+     inkscape:snap-bbox-midpoints="true"
+     inkscape:object-paths="true"
+     inkscape:snap-intersection-paths="true"
+     inkscape:snap-smooth-nodes="true"
+     inkscape:snap-midpoints="true"
+     inkscape:snap-object-midpoints="true"
+     inkscape:snap-center="true"
+     inkscape:snap-text-baseline="true"
+     inkscape:snap-page="true"
+     inkscape:window-width="1920"
+     inkscape:window-height="1017"
+     inkscape:window-x="-8"
+     inkscape:window-y="1072"
+     inkscape:window-maximized="1">
+    <inkscape:grid
+       originy="-59.94301"
+       originx="-51.133667"
+       type="xygrid"
+       id="grid833"
+       dotted="true"
+       units="mm"
+       spacingx="1"
+       spacingy="1" />
+  </sodipodi:namedview>
+  <metadata
+     id="metadata5">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title></dc:title>
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <g
+     transform="translate(-51.133667,-59.94301)"
+     inkscape:label="Layer 1"
+     inkscape:groupmode="layer"
+     id="layer1">
+    <g
+       transform="matrix(0.49504163,0,0,0.49504163,28.887734,26.54317)"
+       id="g1894"
+       style="stroke-width:0.534467">
+      <path
+         id="path8615"
+         d="M 99.75,67.46875 C 71.718268,67.468752 73.46875,79.625 73.46875,79.625 L 73.5,92.21875 h 26.75 V 96 H 62.875 c 0,0 -17.9375,-2.034276 -17.9375,26.25 -2e-6,28.28427 15.65625,27.28125 15.65625,27.28125 h 9.34375 v -13.125 c 0,0 -0.503652,-15.65625 15.40625,-15.65625 15.9099,0 26.53125,0 26.53125,0 0,0 14.90625,0.24096 14.90625,-14.40625 0,-14.647206 0,-24.21875 0,-24.21875 0,-2e-6 2.26318,-14.65625 -27.03125,-14.65625 z M 85,75.9375 c 2.661429,-2e-6 4.8125,2.151071 4.8125,4.8125 2e-6,2.661429 -2.151071,4.8125 -4.8125,4.8125 -2.661429,2e-6 -4.8125,-2.151071 -4.8125,-4.8125 -2e-6,-2.661429 2.151071,-4.8125 4.8125,-4.8125 z"
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#617d8b;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.534467;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none" />
+      <path
+         style="color:#000000;display:inline;overflow:visible;visibility:visible;opacity:1;fill:#8cc04d;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.534467;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;marker:none;marker-start:none;marker-mid:none;marker-end:none"
+         d="m 100.5461,177.31485 c 28.03174,0 26.28125,-12.15625 26.28125,-12.15625 l -0.0312,-12.59375 h -26.75 v -3.78125 h 37.375 c 0,0 17.9375,2.03427 17.9375,-26.25001 10e-6,-28.284267 -15.65625,-27.281247 -15.65625,-27.281247 h -9.34375 v 13.124997 c 0,0 0.50366,15.65625 -15.40625,15.65625 -15.909902,0 -26.531252,0 -26.531252,0 0,0 -14.90625,-0.24096 -14.90625,14.40626 0,14.6472 0,24.21875 0,24.21875 0,0 -2.26318,14.65625 27.031252,14.65625 z m 14.75,-8.46875 c -2.66143,0 -4.8125,-2.15107 -4.8125,-4.8125 0,-2.66143 2.15107,-4.8125 4.8125,-4.8125 2.66143,0 4.8125,2.15107 4.8125,4.8125 1e-5,2.66143 -2.15107,4.8125 -4.8125,4.8125 z"
+         id="path8620" />
+    </g>
+  </g>
+</svg>
diff --git a/docs/api/bo4e.bo.rst b/docs/api/bo4e.bo.rst
index 015b6d80c..b19c699d7 100644
--- a/docs/api/bo4e.bo.rst
+++ b/docs/api/bo4e.bo.rst
@@ -4,6 +4,14 @@ bo4e.bo package
 Submodules
 ----------
 
+bo4e.bo.ansprechpartner module
+------------------------------
+
+.. automodule:: bo4e.bo.ansprechpartner
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.bo.geschaeftsobjekt module
 -------------------------------
 
@@ -36,6 +44,30 @@ bo4e.bo.marktteilnehmer module
    :undoc-members:
    :show-inheritance:
 
+bo4e.bo.messlokation module
+---------------------------
+
+.. automodule:: bo4e.bo.messlokation
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.bo.vertrag module
+----------------------
+
+.. automodule:: bo4e.bo.vertrag
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.bo.zaehler module
+----------------------
+
+.. automodule:: bo4e.bo.zaehler
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Module contents
 ---------------
 
diff --git a/docs/api/bo4e.com.rst b/docs/api/bo4e.com.rst
index 644548411..79b128028 100644
--- a/docs/api/bo4e.com.rst
+++ b/docs/api/bo4e.com.rst
@@ -20,6 +20,14 @@ bo4e.com.com module
    :undoc-members:
    :show-inheritance:
 
+bo4e.com.dienstleistung module
+------------------------------
+
+.. automodule:: bo4e.com.dienstleistung
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.com.externereferenz module
 -------------------------------
 
@@ -36,6 +44,14 @@ bo4e.com.geokoordinaten module
    :undoc-members:
    :show-inheritance:
 
+bo4e.com.hardware module
+------------------------
+
+.. automodule:: bo4e.com.hardware
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.com.katasteradresse module
 -------------------------------
 
@@ -44,6 +60,14 @@ bo4e.com.katasteradresse module
    :undoc-members:
    :show-inheritance:
 
+bo4e.com.menge module
+---------------------
+
+.. automodule:: bo4e.com.menge
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.com.messlokationszuordnung module
 --------------------------------------
 
@@ -52,6 +76,62 @@ bo4e.com.messlokationszuordnung module
    :undoc-members:
    :show-inheritance:
 
+bo4e.com.rufnummer module
+-------------------------
+
+.. automodule:: bo4e.com.rufnummer
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.com.unterschrift module
+----------------------------
+
+.. automodule:: bo4e.com.unterschrift
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.com.vertragskonditionen module
+-----------------------------------
+
+.. automodule:: bo4e.com.vertragskonditionen
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.com.vertragsteil module
+----------------------------
+
+.. automodule:: bo4e.com.vertragsteil
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.com.zaehlwerk module
+-------------------------
+
+.. automodule:: bo4e.com.zaehlwerk
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.com.zeitraum module
+------------------------
+
+.. automodule:: bo4e.com.zeitraum
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.com.zustaendigkeit module
+------------------------------
+
+.. automodule:: bo4e.com.zustaendigkeit
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Module contents
 ---------------
 
diff --git a/docs/api/bo4e.enum.rst b/docs/api/bo4e.enum.rst
index f11a193fb..e12bf356c 100644
--- a/docs/api/bo4e.enum.rst
+++ b/docs/api/bo4e.enum.rst
@@ -36,6 +36,14 @@ bo4e.enum.botyp module
    :undoc-members:
    :show-inheritance:
 
+bo4e.enum.dienstleistungstyp module
+-----------------------------------
+
+.. automodule:: bo4e.enum.dienstleistungstyp
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.enum.energierichtung module
 --------------------------------
 
@@ -60,6 +68,14 @@ bo4e.enum.gebiettyp module
    :undoc-members:
    :show-inheritance:
 
+bo4e.enum.geraetetyp module
+---------------------------
+
+.. automodule:: bo4e.enum.geraetetyp
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.enum.geschaeftspartnerrolle module
 ---------------------------------------
 
@@ -92,6 +108,14 @@ bo4e.enum.marktrolle module
    :undoc-members:
    :show-inheritance:
 
+bo4e.enum.mengeneinheit module
+------------------------------
+
+.. automodule:: bo4e.enum.mengeneinheit
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.enum.netzebene module
 --------------------------
 
@@ -108,6 +132,14 @@ bo4e.enum.rollencodetyp module
    :undoc-members:
    :show-inheritance:
 
+bo4e.enum.rufnummernart module
+------------------------------
+
+.. automodule:: bo4e.enum.rufnummernart
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.enum.sparte module
 -----------------------
 
@@ -116,6 +148,30 @@ bo4e.enum.sparte module
    :undoc-members:
    :show-inheritance:
 
+bo4e.enum.tarifart module
+-------------------------
+
+.. automodule:: bo4e.enum.tarifart
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.enum.themengebiet module
+-----------------------------
+
+.. automodule:: bo4e.enum.themengebiet
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.enum.titel module
+----------------------
+
+.. automodule:: bo4e.enum.titel
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 bo4e.enum.verbrauchsart module
 ------------------------------
 
@@ -124,6 +180,46 @@ bo4e.enum.verbrauchsart module
    :undoc-members:
    :show-inheritance:
 
+bo4e.enum.vertragsart module
+----------------------------
+
+.. automodule:: bo4e.enum.vertragsart
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.enum.vertragsstatus module
+-------------------------------
+
+.. automodule:: bo4e.enum.vertragsstatus
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.enum.zaehlerauspraegung module
+-----------------------------------
+
+.. automodule:: bo4e.enum.zaehlerauspraegung
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.enum.zaehlertyp module
+---------------------------
+
+.. automodule:: bo4e.enum.zaehlertyp
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+bo4e.enum.zeiteinheit module
+----------------------------
+
+.. automodule:: bo4e.enum.zeiteinheit
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
 Module contents
 ---------------
 
diff --git a/docs/api/bo4e.rst b/docs/api/bo4e.rst
index 7d741022e..565a19a6d 100644
--- a/docs/api/bo4e.rst
+++ b/docs/api/bo4e.rst
@@ -10,17 +10,7 @@ Subpackages
    bo4e.bo
    bo4e.com
    bo4e.enum
-
-Submodules
-----------
-
-bo4e.cases module
------------------
-
-.. automodule:: bo4e.cases
-   :members:
-   :undoc-members:
-   :show-inheritance:
+   bo4e.schemata
 
 Module contents
 ---------------
diff --git a/docs/api/bo4e.schemata.rst b/docs/api/bo4e.schemata.rst
new file mode 100644
index 000000000..fa27e9897
--- /dev/null
+++ b/docs/api/bo4e.schemata.rst
@@ -0,0 +1,21 @@
+bo4e.schemata package
+=====================
+
+Submodules
+----------
+
+bo4e.schemata.caseconverterschema module
+----------------------------------------
+
+.. automodule:: bo4e.schemata.caseconverterschema
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: bo4e.schemata
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/conf.py b/docs/conf.py
index 2e1973a4a..a7cf9e918 100644
--- a/docs/conf.py
+++ b/docs/conf.py
@@ -8,16 +8,11 @@
 # All configuration values have a default; values that are commented out
 # serve to show the default.
 
-import os
-import sys
 import inspect
+import os
 import shutil
-from pkg_resources import get_distribution
+import sys
 
-# Get version information for the package documentation
-# which is published on read the docs: https://bo4e-python.readthedocs.io/en/latest/
-release = get_distribution("bo4e").version
-version = ".".join(release.split(".")[:2])
 
 __location__ = os.path.join(os.getcwd(), os.path.dirname(inspect.getfile(inspect.currentframe())))
 
@@ -245,7 +240,6 @@
 # Output file base name for HTML help builder.
 htmlhelp_basename = "bo4e-doc"
 
-
 # -- Options for LaTeX output --------------------------------------------------
 
 latex_elements = {
diff --git a/docs/index.rst b/docs/index.rst
index 562d5c5d1..e38ebf77c 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -2,25 +2,7 @@
 bo4e
 =============
 
-This is the documentation of **bo4e**.
-
-.. note::
-
-    This is the main page of your project's `Sphinx`_ documentation.
-    It is formatted in `reStructuredText`_. Add additional pages
-    by creating rst-files in ``docs`` and adding them to the `toctree`_ below.
-    Use then `references`_ in order to link them from this page, e.g.
-    :ref:`authors` and :ref:`changes`.
-
-    It is also possible to refer to the documentation of other Python packages
-    with the `Python domain syntax`_. By default you can reference the
-    documentation of `Sphinx`_, `Python`_, `NumPy`_, `SciPy`_, `matplotlib`_,
-    `Pandas`_, `Scikit-Learn`_. You can add more by extending the
-    ``intersphinx_mapping`` in your Sphinx's ``conf.py``.
-
-    The pretty useful extension `autodoc`_ is activated by default and lets
-    you include documentation from docstrings. Docstrings can be written in
-    `Google style`_ (recommended!), `NumPy style`_ and `classical style`_.
+This is the documentation of **bo4e** python package.
 
 
 Contents
diff --git a/pyproject.toml b/pyproject.toml
index 78443860e..4ff0d6ae3 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,3 +6,13 @@ build-backend = "setuptools.build_meta"
 line-length = 120
 
 [tool.setuptools_scm]
+
+[tool.pytest.ini_options]
+testpaths = "tests"
+markers = ["datafiles: load datafiles"]
+
+[tool.pylint.'MESSAGES CONTROL']
+max-line-length = 120
+
+[tool.isort]
+line_length = 120
\ No newline at end of file
diff --git a/requirements/base.in b/requirements.in
similarity index 79%
rename from requirements/base.in
rename to requirements.in
index f2495a2c7..e51de1085 100644
--- a/requirements/base.in
+++ b/requirements.in
@@ -2,4 +2,4 @@ attrs
 marshmallow
 marshmallow-enum
 iso3166
-stringcase
\ No newline at end of file
+stringcase
diff --git a/requirements.txt b/requirements.txt
new file mode 100644
index 000000000..5420b8f0b
--- /dev/null
+++ b/requirements.txt
@@ -0,0 +1,18 @@
+#
+# This file is autogenerated by pip-compile with python 3.9
+# To update, run:
+#
+#    pip-compile requirements.in
+#
+attrs==21.2.0
+    # via -r requirements.in
+iso3166==1.0.1
+    # via -r requirements.in
+marshmallow==3.12.1
+    # via
+    #   -r requirements.in
+    #   marshmallow-enum
+marshmallow-enum==1.5.1
+    # via -r requirements.in
+stringcase==1.2.0
+    # via -r requirements.in
diff --git a/requirements/base.txt b/requirements/base.txt
deleted file mode 100644
index 49ae903ec..000000000
--- a/requirements/base.txt
+++ /dev/null
@@ -1,18 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile requirements/base.in
-#
-attrs==20.3.0
-    # via -r requirements/base.in
-iso3166==1.0.1
-    # via -r requirements/base.in
-marshmallow-enum==1.5.1
-    # via -r requirements/base.in
-marshmallow==3.9.1
-    # via
-    #   -r requirements/base.in
-    #   marshmallow-enum
-stringcase==1.2.0
-    # via -r requirements/base.in
diff --git a/requirements/dev.in b/requirements/dev.in
deleted file mode 100644
index aeee099b4..000000000
--- a/requirements/dev.in
+++ /dev/null
@@ -1,7 +0,0 @@
--r base.in
--r docs.in
--r tests.in
-black
-pip-tools
-flake8
-tox
\ No newline at end of file
diff --git a/requirements/dev.txt b/requirements/dev.txt
deleted file mode 100644
index b419910e5..000000000
--- a/requirements/dev.txt
+++ /dev/null
@@ -1,154 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile requirements/dev.in
-#
-alabaster==0.7.12
-    # via sphinx
-appdirs==1.4.4
-    # via
-    #   black
-    #   virtualenv
-atomicwrites==1.4.0
-    # via pytest
-attrs==20.2.0
-    # via
-    #   -r requirements\base.in
-    #   pytest
-babel==2.8.0
-    # via sphinx
-black==20.8b1
-    # via -r requirements/dev.in
-certifi==2020.6.20
-    # via requests
-chardet==3.0.4
-    # via requests
-click==7.1.2
-    # via
-    #   black
-    #   pip-tools
-colorama==0.4.4
-    # via
-    #   pytest
-    #   sphinx
-    #   tox
-distlib==0.3.1
-    # via virtualenv
-docutils==0.16
-    # via sphinx
-filelock==3.0.12
-    # via
-    #   tox
-    #   virtualenv
-flake8==3.8.4
-    # via
-    #   -r requirements/dev.in
-    #   -r requirements\tests.in
-idna==2.10
-    # via requests
-imagesize==1.2.0
-    # via sphinx
-iniconfig==1.1.1
-    # via pytest
-iso3166==1.0.1
-    # via -r requirements\base.in
-jinja2==2.11.2
-    # via sphinx
-markupsafe==1.1.1
-    # via jinja2
-marshmallow-enum==1.5.1
-    # via -r requirements\base.in
-marshmallow==3.9.1
-    # via
-    #   -r requirements\base.in
-    #   marshmallow-enum
-mccabe==0.6.1
-    # via flake8
-mypy-extensions==0.4.3
-    # via black
-packaging==20.4
-    # via
-    #   pytest
-    #   sphinx
-    #   tox
-pathspec==0.8.0
-    # via black
-pip-tools==5.3.1
-    # via -r requirements/dev.in
-pluggy==0.13.1
-    # via
-    #   pytest
-    #   tox
-py==1.9.0
-    # via
-    #   pytest
-    #   pytest-datafiles
-    #   tox
-pycodestyle==2.6.0
-    # via flake8
-pyflakes==2.2.0
-    # via flake8
-pygments==2.7.1
-    # via sphinx
-pyparsing==2.4.7
-    # via packaging
-pytest-datafiles==2.0
-    # via -r requirements\tests.in
-pytest==6.1.1
-    # via
-    #   -r requirements\tests.in
-    #   pytest-datafiles
-pytz==2020.1
-    # via babel
-regex==2020.10.15
-    # via black
-requests==2.24.0
-    # via sphinx
-six==1.15.0
-    # via
-    #   packaging
-    #   pip-tools
-    #   tox
-    #   virtualenv
-snowballstemmer==2.0.0
-    # via sphinx
-sphinx-rtd-theme==0.5.0
-    # via -r requirements\docs.in
-sphinx==3.2.1
-    # via
-    #   -r requirements\docs.in
-    #   sphinx-rtd-theme
-sphinxcontrib-applehelp==1.0.2
-    # via sphinx
-sphinxcontrib-devhelp==1.0.2
-    # via sphinx
-sphinxcontrib-htmlhelp==1.0.3
-    # via sphinx
-sphinxcontrib-jsmath==1.0.1
-    # via sphinx
-sphinxcontrib-qthelp==1.0.3
-    # via sphinx
-sphinxcontrib-serializinghtml==1.1.4
-    # via sphinx
-stringcase==1.2.0
-    # via -r requirements\base.in
-toml==0.10.1
-    # via
-    #   black
-    #   pytest
-    #   tox
-tox==3.20.1
-    # via -r requirements/dev.in
-typed-ast==1.4.1
-    # via black
-typing-extensions==3.7.4.3
-    # via black
-urllib3==1.25.11
-    # via requests
-virtualenv==20.0.35
-    # via tox
-
-# The following packages are considered to be unsafe in a requirements file:
-# pip
-# setuptools
diff --git a/requirements/docs.in b/requirements/docs.in
deleted file mode 100644
index 5aa40fdb5..000000000
--- a/requirements/docs.in
+++ /dev/null
@@ -1,3 +0,0 @@
--r base.in
-Sphinx
-sphinx_rtd_theme
\ No newline at end of file
diff --git a/requirements/docs.txt b/requirements/docs.txt
deleted file mode 100644
index 1f5cd771e..000000000
--- a/requirements/docs.txt
+++ /dev/null
@@ -1,75 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile requirements/docs.in
-#
-alabaster==0.7.12
-    # via sphinx
-attrs==20.3.0
-    # via -r requirements\base.in
-babel==2.8.0
-    # via sphinx
-certifi==2020.6.20
-    # via requests
-chardet==3.0.4
-    # via requests
-colorama==0.4.4
-    # via sphinx
-docutils==0.16
-    # via sphinx
-idna==2.10
-    # via requests
-imagesize==1.2.0
-    # via sphinx
-iso3166==1.0.1
-    # via -r requirements\base.in
-jinja2==2.11.2
-    # via sphinx
-markupsafe==1.1.1
-    # via jinja2
-marshmallow-enum==1.5.1
-    # via -r requirements\base.in
-marshmallow==3.9.1
-    # via
-    #   -r requirements\base.in
-    #   marshmallow-enum
-packaging==20.4
-    # via sphinx
-pygments==2.7.1
-    # via sphinx
-pyparsing==2.4.7
-    # via packaging
-pytz==2020.1
-    # via babel
-requests==2.24.0
-    # via sphinx
-six==1.15.0
-    # via packaging
-snowballstemmer==2.0.0
-    # via sphinx
-sphinx-rtd-theme==0.5.0
-    # via -r requirements/docs.in
-sphinx==3.2.1
-    # via
-    #   -r requirements/docs.in
-    #   sphinx-rtd-theme
-sphinxcontrib-applehelp==1.0.2
-    # via sphinx
-sphinxcontrib-devhelp==1.0.2
-    # via sphinx
-sphinxcontrib-htmlhelp==1.0.3
-    # via sphinx
-sphinxcontrib-jsmath==1.0.1
-    # via sphinx
-sphinxcontrib-qthelp==1.0.3
-    # via sphinx
-sphinxcontrib-serializinghtml==1.1.4
-    # via sphinx
-stringcase==1.2.0
-    # via -r requirements\base.in
-urllib3==1.25.11
-    # via requests
-
-# The following packages are considered to be unsafe in a requirements file:
-# setuptools
diff --git a/setup.cfg b/setup.cfg
index 12bcbd019..9b5fd13d9 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,15 +1,15 @@
 [metadata]
 url = https://github.com/Hochfrequenz/BO4E-python
-project-urls =
+project_urls =
     Documentation = https://bo4e-python.readthedocs.io/en/latest/
     Code = https://github.com/Hochfrequenz/BO4E-python
     Issue tracker = https://github.com/Hochfrequenz/BO4E-python/issues
 license = mit
 author = Kevin Krechan
-author-email = kevin.krechan@hochfrequenz.de
+author_email = kevin.krechan@hochfrequenz.de
 description = Python Library that implements the BO4E Standard.
-long-description = file: README.rst
-long-description-content-type = text/x-rst; charset=UTF-8
+long_description = file: README.rst
+long_description_content_type = text/x-rst; charset=UTF-8
 platforms = any
 classifiers =
     Development Status :: 4 - Beta
@@ -17,14 +17,14 @@ classifiers =
     Operating System :: OS Independent
     Programming Language :: Python
     Programming Language :: Python :: 3.8
-    
+
 [options]
 zip_safe = False
 packages = find:
 package_dir = = src
 include_package_data = True
 python_requires = >=3.8
-install_requires = 
+install_requires =
     attr
     iso3166
     marshmallow
@@ -35,6 +35,3 @@ install_requires =
 where = src
 exclude =
     tests
-
-[tool:pytest]
-testpaths = tests
diff --git a/setup.py b/setup.py
index f4beb778a..038f6b271 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,6 @@
 
 from setuptools import setup
 
-
 min_version = (3, 8)
 
 if sys.version_info < min_version:
@@ -15,5 +14,4 @@
     )
     sys.exit(error)
 
-
 setup(name="bo4e", use_scm_version=True)
diff --git a/src/bo4e/bo/ansprechpartner.py b/src/bo4e/bo/ansprechpartner.py
new file mode 100644
index 000000000..bd745b5b6
--- /dev/null
+++ b/src/bo4e/bo/ansprechpartner.py
@@ -0,0 +1,63 @@
+"""
+Contains Ansprechpartner class
+and corresponding marshmallow schema for de-/serialization
+"""
+import attr
+from marshmallow import fields
+from marshmallow_enum import EnumField
+from bo4e.com.rufnummer import Rufnummer, RufnummerSchema
+from bo4e.com.adresse import Adresse, AdresseSchema
+from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.bo.geschaeftspartner import Geschaeftspartner, GeschaeftspartnerSchema
+from bo4e.com.zustaendigkeit import Zustaendigkeit, ZustaendigkeitSchema
+from bo4e.enum.anrede import Anrede
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.titel import Titel
+
+# pylint: disable=too-many-instance-attributes, too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Ansprechpartner(Geschaeftsobjekt):
+    """
+    Object containing information about a Ansprechpartner
+    """
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.ANSPRECHPARTNER)
+    nachname: str
+    geschaeftspartner: Geschaeftspartner
+
+    # optional attributes
+    anrede: Anrede = attr.ib(default=None)
+    individuelle_anrede: str = attr.ib(default=None)
+    titel: Titel = attr.ib(default=None)
+    vorname: str = attr.ib(default=None)
+    e_mail_adresse: str = attr.ib(default=None)
+    kommentar: str = attr.ib(default=None)
+    adresse: Adresse = attr.ib(default=None)
+    rufnummer: Rufnummer = attr.ib(default=None)
+    zustaendigkeit: Zustaendigkeit = attr.ib(default=None)
+
+
+class AnsprechpartnerSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Ansprechpartner.
+    """
+
+    # class_name is needed to use the correct schema for deserialisation.
+    # see function `deserialize` in geschaeftsobjekt.py
+    class_name = Ansprechpartner
+
+    # required attributes
+    nachname = fields.Str()
+    geschaeftspartner = fields.Nested(GeschaeftspartnerSchema)
+
+    # optional attributes
+    anrede = EnumField(Anrede, missing=None)
+    individuelle_anrede = fields.Str(missing=None)
+    titel = EnumField(Titel, missing=None)
+    vorname = fields.Str(missing=None)
+    e_mail_adresse = fields.Str(missing=None)
+    kommentar = fields.Str(missing=None)
+    adresse = fields.Nested(AdresseSchema, missing=None)
+    rufnummer = fields.Nested(RufnummerSchema, missing=None)
+    zustaendigkeit = fields.Nested(ZustaendigkeitSchema, missing=None)
diff --git a/src/bo4e/bo/geschaeftsobjekt.py b/src/bo4e/bo/geschaeftsobjekt.py
index 41795cbf2..b0b04614f 100644
--- a/src/bo4e/bo/geschaeftsobjekt.py
+++ b/src/bo4e/bo/geschaeftsobjekt.py
@@ -1,12 +1,16 @@
+"""
+Contains base class for all business objects
+and corresponding marshmallow schema for de-/serialization
+"""
+# pylint: disable=unused-argument, too-few-public-methods
 from typing import List, Optional
 
 import attr
-from marshmallow import Schema, fields, post_load
+from marshmallow import fields, post_load
 from marshmallow_enum import EnumField
-from bo4e.cases import JavaScriptMixin
-
 from bo4e.com.externereferenz import ExterneReferenz, ExterneReferenzSchema
 from bo4e.enum.botyp import BoTyp
+from bo4e.schemata.caseconverterschema import CaseConverterSchema
 
 
 @attr.s(auto_attribs=True, kw_only=True)
@@ -25,10 +29,10 @@ class Geschaeftsobjekt:
     )
 
 
-class GeschaeftsobjektSchema(Schema, JavaScriptMixin):
+class GeschaeftsobjektSchema(CaseConverterSchema):
     """
     This is an "abstract" class.
-    All business objects are inherited from this class.
+    All business objects schemata do inherit from this class.
     """
 
     # class_name is needed to use the correct schema for deserialization.
@@ -42,5 +46,6 @@ class GeschaeftsobjektSchema(Schema, JavaScriptMixin):
     externe_referenzen = fields.List(fields.Nested(ExterneReferenzSchema), missing=None)
 
     @post_load
-    def deserialise(self, data, **kwargs):
+    def deserialize(self, data, **kwargs):
+        """Deserialize JSON to python object."""
         return type(self).class_name(**data)
diff --git a/src/bo4e/bo/geschaeftspartner.py b/src/bo4e/bo/geschaeftspartner.py
index 601a33467..cc996c3d1 100644
--- a/src/bo4e/bo/geschaeftspartner.py
+++ b/src/bo4e/bo/geschaeftspartner.py
@@ -1,15 +1,19 @@
-import attr
+"""
+Contains Geschaeftspartner class
+and corresponding marshmallow schema for de-/serialization
+"""
+# pylint: disable=too-many-instance-attributes, too-few-public-methods
+from typing import List
 
+import attr
 from marshmallow import fields
 from marshmallow_enum import EnumField
-from typing import List
-
 from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
 from bo4e.com.adresse import Adresse, AdresseSchema
 from bo4e.enum.anrede import Anrede
-from bo4e.enum.kontaktart import Kontaktart
-from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
 from bo4e.enum.botyp import BoTyp
+from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
+from bo4e.enum.kontaktart import Kontaktart
 
 
 @attr.s(auto_attribs=True, kw_only=True)
@@ -39,8 +43,12 @@ class Geschaeftspartner(Geschaeftsobjekt):
 
 
 class GeschaeftspartnerSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Geschaeftspartner.
+    """
+
     # class_name is needed to use the correct schema for deserialisation.
-    # see function `deserialise` in geschaeftsobjekt.py
+    # see function `deserialize` in geschaeftsobjekt.py
     class_name = Geschaeftspartner
 
     # required attributes
diff --git a/src/bo4e/bo/marktlokation.py b/src/bo4e/bo/marktlokation.py
index 90c7dbaba..93f8075db 100644
--- a/src/bo4e/bo/marktlokation.py
+++ b/src/bo4e/bo/marktlokation.py
@@ -1,36 +1,39 @@
+"""
+Contains Marktlokation class
+and corresponding marshmallow schema for de-/serialization
+"""
 import re
 
 import attr
 from marshmallow import fields
 from marshmallow_enum import EnumField
-
 from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
 from bo4e.bo.geschaeftspartner import Geschaeftspartner, GeschaeftspartnerSchema
 from bo4e.com.adresse import Adresse, AdresseSchema
 from bo4e.com.geokoordinaten import Geokoordinaten, GeokoordinatenSchema
 from bo4e.com.katasteradresse import Katasteradresse, KatasteradresseSchema
-from bo4e.com.messlokationszuordnung import (
-    Messlokationszuordnung,
-    MesslokationszuordnungSchema,
-)
-from bo4e.enum.sparte import Sparte
+from bo4e.com.messlokationszuordnung import Messlokationszuordnung, MesslokationszuordnungSchema
+from bo4e.enum.bilanzierungsmethode import Bilanzierungsmethode
 from bo4e.enum.botyp import BoTyp
 from bo4e.enum.energierichtung import Energierichtung
-from bo4e.enum.bilanzierungsmethode import Bilanzierungsmethode
-from bo4e.enum.verbrauchsart import Verbrauchsart
-from bo4e.enum.netzebene import Netzebene
-from bo4e.enum.gebiettyp import Gebiettyp
 from bo4e.enum.gasqualitaet import Gasqualitaet
+from bo4e.enum.gebiettyp import Gebiettyp
+from bo4e.enum.netzebene import Netzebene
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.verbrauchsart import Verbrauchsart
+
 
 _malo_id_pattern = re.compile(r"^[1-9][\d]{10}$")
 
 
+# pylint: disable=too-many-instance-attributes, too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class Marktlokation(Geschaeftsobjekt):
     """
     Object containing information about a Marktlokation
     """
 
+    # pylint: disable=unused-argument, no-self-use
     def _validate_marktlokations_id(self, marktlokations_id_attribute, value):
         if not value:
             raise ValueError("The marktlokations_id must not be empty.")
@@ -39,6 +42,7 @@ def _validate_marktlokations_id(self, marktlokations_id_attribute, value):
         expected_checksum = Marktlokation._get_checksum(value)
         actual_checksum = value[10:11]
         if expected_checksum != actual_checksum:
+            # pylint: disable=line-too-long
             raise ValueError(
                 f"The marktlokations_id '{value}' has checksum '{actual_checksum}' but '{expected_checksum}' was expected."
             )
@@ -72,6 +76,7 @@ def _validate_marktlokations_id(self, marktlokations_id_attribute, value):
     @geoadresse.validator
     @katasterinformation.validator
     def validate_address_info(self, address_attribute, value):
+        """Checks that there is one and only one valid adress given."""
         all_address_attributes = [
             self.lokationsadresse,
             self.geoadresse,
@@ -98,18 +103,23 @@ def _get_checksum(malo_id: str) -> str:
         # start counting at 1 to be consistent with the above description
         # of "even" and "odd" but stop at tenth digit.
         for i in range(1, 11):
-            s = malo_id[i - 1 : i]
+            digit = malo_id[i - 1 : i]
             if i % 2 - 1 == 0:
-                odd_checksum += int(s)
+                odd_checksum += int(digit)
             else:
-                even_checksum += 2 * int(s)
+                even_checksum += 2 * int(digit)
         result: int = (10 - ((even_checksum + odd_checksum) % 10)) % 10
         return str(result)
 
 
 class MarktlokationSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Marktlokation.
+    Inherits from GeschaeftsobjektSchema.
+    """
+
     # class_name is needed to use the correct schema for deserialisation.
-    # see function `deserialise` in geschaeftsobjekt.py
+    # see function `deserialize` in geschaeftsobjekt.py
     class_name = Marktlokation
 
     # required attributes
diff --git a/src/bo4e/bo/marktteilnehmer.py b/src/bo4e/bo/marktteilnehmer.py
index 2ece9aa58..c253a428f 100644
--- a/src/bo4e/bo/marktteilnehmer.py
+++ b/src/bo4e/bo/marktteilnehmer.py
@@ -1,15 +1,18 @@
+"""
+Contains Marktteilnehmer class
+and corresponding marshmallow schema for de-/serialization
+"""
 import attr
-from attr.validators import matches_re
-
 from marshmallow import fields
 from marshmallow_enum import EnumField
-
+from attr.validators import matches_re
 from bo4e.bo.geschaeftspartner import Geschaeftspartner, GeschaeftspartnerSchema
 from bo4e.enum.botyp import BoTyp
 from bo4e.enum.marktrolle import Marktrolle
 from bo4e.enum.rollencodetyp import Rollencodetyp
 
 
+# pylint: disable=too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class Marktteilnehmer(Geschaeftspartner):
     """
@@ -27,8 +30,12 @@ class Marktteilnehmer(Geschaeftspartner):
 
 
 class MarktteilnehmerSchema(GeschaeftspartnerSchema):
+    """
+    Schema for de-/serialization of Marktteilnehmer.
+    """
+
     # class_name is needed to use the correct schema for deserialization.
-    # see function `deserialise` in geschaeftsobjekt.py
+    # see function `deserialize` in geschaeftsobjekt.py
     class_name = Marktteilnehmer
 
     # required attributes
diff --git a/src/bo4e/bo/messlokation.py b/src/bo4e/bo/messlokation.py
new file mode 100644
index 000000000..518df6c59
--- /dev/null
+++ b/src/bo4e/bo/messlokation.py
@@ -0,0 +1,128 @@
+"""
+Contains Messlokation class
+and corresponding marshmallow schema for de-/serialization
+"""
+import re
+from typing import List, Optional
+
+import attr
+from iso3166 import countries
+from marshmallow import fields
+from marshmallow_enum import EnumField
+
+from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.bo.zaehler import Zaehler, ZaehlerSchema
+from bo4e.com.adresse import Adresse, AdresseSchema
+from bo4e.com.dienstleistung import Dienstleistung, DienstleistungSchema
+from bo4e.com.geokoordinaten import Geokoordinaten, GeokoordinatenSchema
+from bo4e.com.hardware import Hardware, HardwareSchema
+from bo4e.com.katasteradresse import Katasteradresse, KatasteradresseSchema
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.netzebene import Netzebene
+from bo4e.enum.sparte import Sparte
+
+# Structure of a Messlokations-ID
+# Ländercode nach DIN ISO 3166 (2 Stellen)
+# Verteilnetzbetreiber (6 Stellen)
+# Postleitzahl (5 Stellen)
+# Zählpunktnummer (20 Stellen alphanumerisch)
+# source: https://de.wikipedia.org/wiki/Z%C3%A4hlpunkt#Struktur_der_Z%C3%A4hlpunktbezeichnung
+
+_melo_id_pattern = re.compile(r"^[A-Z]{2}\d{6}\d{5}[A-Z\d]{20}$")
+
+
+# pylint: disable=too-many-instance-attributes, too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Messlokation(Geschaeftsobjekt):
+    """
+    Object containing information about a Messlokation
+    """
+
+    # pylint: disable=unused-argument, no-self-use
+    def _validate_messlokations_id(self, messlokations_id_attribute, value):
+        if not value:
+            raise ValueError("The messlokations_id must not be empty.")
+        if not _melo_id_pattern.match(value):
+            raise ValueError(f"The messlokations_id '{value}' does not match {_melo_id_pattern.pattern}")
+        if not value[0:2] in countries:
+            raise ValueError(f"The country code '{value[0:2]}' is not a valid country code")
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.MESSLOKATION)
+    messlokations_id: str = attr.ib(validator=_validate_messlokations_id)
+    sparte: Sparte
+
+    # optional attributes
+    netzebene_messung: Optional[Netzebene] = attr.ib(default=None)
+    messgebietnr: Optional[str] = attr.ib(default=None)
+    geraete: Optional[List[Hardware]] = attr.ib(default=None)
+    messdienstleistung: Optional[List[Dienstleistung]] = attr.ib(default=None)
+    messlokationszaehler: Optional[List[Zaehler]] = attr.ib(default=None)
+
+    # only one of the following two optional codenr attributes can be set
+    grundzustaendiger_msb_codenr: Optional[str] = attr.ib(default=None)
+    grundzustaendiger_msbim_codenr: Optional[str] = attr.ib(default=None)
+
+    # only one of the following three optional address attributes can be set
+    messadresse: Optional[Adresse] = attr.ib(default=None)
+    geoadresse: Optional[Geokoordinaten] = attr.ib(default=None)
+    katasterinformation: Optional[Katasteradresse] = attr.ib(default=None)
+
+    @messadresse.validator
+    @geoadresse.validator
+    @katasterinformation.validator
+    def validate_address_info(self, address_attribute, value):
+        """Checks that if an address is given, that there is only one valid address given"""
+        all_address_attributes = [
+            self.messadresse,
+            self.geoadresse,
+            self.katasterinformation,
+        ]
+        amount_of_given_address_infos = len([i for i in all_address_attributes if i is not None])
+        if amount_of_given_address_infos > 1:
+            raise ValueError("More than one address information is given.")
+
+    @grundzustaendiger_msb_codenr.validator
+    @grundzustaendiger_msbim_codenr.validator
+    def validate_grundzustaendiger_x_codenr(self, attribute, value):
+        """Checks that if a codenr is given, that there is only one valid codenr given."""
+        all_grundzustaendiger_x_codenr_attributes = [
+            self.grundzustaendiger_msb_codenr,
+            self.grundzustaendiger_msbim_codenr,
+        ]
+        amount_of_given_grundzustaendiger_x_codenr = len(
+            [i for i in all_grundzustaendiger_x_codenr_attributes if i is not None]
+        )
+        if amount_of_given_grundzustaendiger_x_codenr > 1:
+            raise ValueError("More than one codenr is given.")
+
+
+class MesslokationSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Marktlokation.
+    Inherits from GeschaeftsobjektSchema.
+    """
+
+    # class_name is needed to use the correct schema for deserialisation.
+    # see function `deserialize` in geschaeftsobjekt.py
+    class_name = Messlokation
+
+    # required attributes
+    messlokations_id = fields.Str()
+    sparte = EnumField(Sparte)
+
+    # optional attributes
+    netzebene_messung = EnumField(Netzebene, missing=None)
+    messgebietnr = fields.Str(missing=None)
+    geraete = fields.List(fields.Nested(HardwareSchema), missing=None)  #: List[Hardware]
+    messdienstleistung = fields.List(fields.Nested(DienstleistungSchema), missing=None)  #: List[Dienstleistung]
+    messlokationszaehler = fields.List(fields.Nested(ZaehlerSchema), missing=None)
+
+    # only one of the following two optional codenr attributes can be set
+    grundzustaendiger_msb_codenr = fields.Str(missing=None)
+    grundzustaendiger_msbim_codenr = fields.Str(missing=None)
+
+    # only one of the following three optional attributes can be set
+    messadresse = fields.Nested(AdresseSchema, missing=None)
+    geoadresse = fields.Nested(GeokoordinatenSchema, missing=None)
+    katasterinformation = fields.Nested(KatasteradresseSchema, missing=None)
diff --git a/src/bo4e/bo/vertrag.py b/src/bo4e/bo/vertrag.py
new file mode 100644
index 000000000..6b55b703c
--- /dev/null
+++ b/src/bo4e/bo/vertrag.py
@@ -0,0 +1,83 @@
+"""
+Contains Vertrag class
+and corresponding marshmallow schema for de-/serialization
+"""
+from datetime import datetime
+from typing import List, Optional
+import attr
+from marshmallow import fields
+from marshmallow_enum import EnumField
+
+from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.bo.geschaeftspartner import Geschaeftspartner, GeschaeftspartnerSchema
+from bo4e.com.vertragsteil import Vertragsteil, VertragsteilSchema
+from bo4e.com.vertragskonditionen import Vertragskonditionen, VertragskonditionenSchema
+from bo4e.com.unterschrift import Unterschrift, UnterschriftSchema
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.vertragsart import Vertragsart
+from bo4e.enum.vertragsstatus import Vertragsstatus
+from bo4e.enum.sparte import Sparte
+
+# pylint: disable=unused-argument
+def at_least_one_vertragsteil(instance, attribute, value):
+    """
+    Ensures that the Vertrag has at least one entry in the Vertragsteile list.
+    """
+    if len(value) == 0:
+        raise ValueError("The Vertrag must have at least 1 Vertragsteil")
+
+
+# pylint: disable=too-many-instance-attributes, too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Vertrag(Geschaeftsobjekt):
+    """
+    Modell für die Abbildung von Vertragsbeziehungen.
+    Das Objekt dient dazu, alle Arten von Verträgen, die in der Energiewirtschaft Verwendung finden, abzubilden.
+    """
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.VERTRAG)
+    vertragsnummer: str = attr.ib(validator=attr.validators.instance_of(str))
+    vertragsart: Vertragsart = attr.ib(validator=attr.validators.instance_of(Vertragsart))
+    vertragsstatus: Vertragsstatus = attr.ib(validator=attr.validators.instance_of(Vertragsstatus))
+    sparte: Sparte = attr.ib(validator=attr.validators.instance_of(Sparte))
+    vertragsbeginn: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
+    vertragsende: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
+
+    vertragspartner1: Geschaeftspartner = attr.ib(validator=attr.validators.instance_of(Geschaeftspartner))
+    vertragspartner2: Geschaeftspartner = attr.ib(validator=attr.validators.instance_of(Geschaeftspartner))
+    vertragsteile: List[Vertragsteil] = attr.ib(validator=at_least_one_vertragsteil)
+
+    # optional attributes
+    beschreibung: Optional[str] = attr.ib(default=None)
+    vertragskonditionen: Optional[Vertragskonditionen] = attr.ib(default=None)
+    unterzeichnervp1: Optional[List[Unterschrift]] = attr.ib(default=None)
+    unterzeichnervp2: Optional[List[Unterschrift]] = attr.ib(default=None)
+
+
+class VertragSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Vertrag.
+    """
+
+    # class_name is needed to use the correct schema for deserialisation.
+    # see function `deserialize` in geschaeftsobjekt.py
+    class_name = Vertrag
+
+    # required attributes
+    vertragsnummer = fields.Str()
+    vertragsart = EnumField(Vertragsart)
+    vertragsstatus = EnumField(Vertragsstatus)
+    sparte = EnumField(Sparte)
+    vertragsbeginn = fields.DateTime()
+    vertragsende = fields.DateTime()
+
+    vertragspartner1 = fields.Nested(GeschaeftspartnerSchema)
+    vertragspartner2 = fields.Nested(GeschaeftspartnerSchema)
+    vertragsteile = fields.Nested(VertragsteilSchema, many=True)
+
+    # optional attributes
+    beschreibung = fields.Str(missing=None)
+    vertragskonditionen = fields.Nested(VertragskonditionenSchema, missing=None)
+    unterzeichnervp1 = fields.Nested(UnterschriftSchema, missing=None, many=True)
+    unterzeichnervp2 = fields.Nested(UnterschriftSchema, missing=None, many=True)
diff --git a/src/bo4e/bo/zaehler.py b/src/bo4e/bo/zaehler.py
new file mode 100644
index 000000000..70eba8a55
--- /dev/null
+++ b/src/bo4e/bo/zaehler.py
@@ -0,0 +1,80 @@
+"""
+Contains Zaehler class
+and corresponding marshmallow schema for de-/serialization
+"""
+from datetime import datetime
+from decimal import Decimal
+from typing import List, Optional
+
+import attr
+from marshmallow import fields
+from marshmallow_enum import EnumField
+
+from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
+from bo4e.bo.geschaeftspartner import Geschaeftspartner, GeschaeftspartnerSchema
+from bo4e.com.zaehlwerk import Zaehlwerk, ZaehlwerkSchema
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.tarifart import Tarifart
+from bo4e.enum.zaehlerauspraegung import Zaehlerauspraegung
+from bo4e.enum.zaehlertyp import Zaehlertyp
+
+# pylint: disable=unused-argument
+def at_least_one_zaehlwerk(instance, attribute, value):
+    """
+    Ensures that the Zaehler has at least one entry in the zaehlwerke list.
+    """
+    if len(value) == 0:
+        raise ValueError("The Zaehler must have at least 1 Zaehlwerk")
+
+
+# pylint: disable=too-many-instance-attributes, too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Zaehler(Geschaeftsobjekt):
+    """
+    Object containing information about a meter/"Zaehler".
+    """
+
+    # required attributes
+    bo_typ: BoTyp = attr.ib(default=BoTyp.ZAEHLER)
+    zaehlernummer: str = attr.ib(
+        validator=attr.validators.instance_of(str)
+    )  # Nummerierung des Zählers, vergeben durch den Messstellenbetreiber
+    sparte: Sparte
+    zaehlerauspraegung: Zaehlerauspraegung
+    zaehlertyp: Zaehlertyp
+    zaehlwerke: List[Zaehlwerk] = attr.ib(validator=at_least_one_zaehlwerk)
+    tarifart: Tarifart  # Spezifikation bezüglich unterstützter Tarifarten
+
+    # optional attributes
+    zaehlerkonstante: Optional[Decimal] = attr.ib(default=None)  # Zählerkonstante auf dem Zähler
+    eichung_bis: Optional[datetime] = attr.ib(default=None)  # Bis zu diesem Datum ist der Zähler geeicht.
+    letzte_eichung: Optional[datetime] = attr.ib(
+        default=None
+    )  # Zu diesem Datum fand die letzte Eichprüfung des Zählers statt.
+    zaehlerhersteller: Optional[Geschaeftspartner] = attr.ib(default=None)  # Der Hersteller des Zählers.
+
+
+class ZaehlerSchema(GeschaeftsobjektSchema):
+    """
+    Schema for de-/serialization of Zaehler.
+    """
+
+    # class_name is needed to use the correct schema for deserialisation.
+    # see function `deserialize` in geschaeftsobjekt.py
+    class_name = Zaehler
+
+    # required attributes
+    zaehlernummer = fields.Str()
+    sparte = EnumField(Sparte)
+    zaehlerauspraegung = EnumField(Zaehlerauspraegung)
+    zaehlertyp = EnumField(Zaehlertyp)
+    tarifart = EnumField(Tarifart)
+    zaehlwerke = fields.Nested(ZaehlwerkSchema, many=True)
+
+    # optional attributes
+    zaehlerkonstante = fields.Decimal(missing=None, as_string=True)
+    eichung_bis = fields.DateTime(missing=None)
+    letzte_eichung = fields.DateTime(missing=None)
+
+    zaehlerhersteller = fields.Nested(GeschaeftspartnerSchema, missing=None)
diff --git a/src/bo4e/cases.py b/src/bo4e/cases.py
deleted file mode 100644
index dc64abae0..000000000
--- a/src/bo4e/cases.py
+++ /dev/null
@@ -1,17 +0,0 @@
-from stringcase import camelcase, snakecase
-from marshmallow import pre_load, post_dump
-
-
-class JavaScriptMixin:
-    """
-    This class is used so that we always use snake_case in the Python world and CamelCase in the JSON (Javascript world).
-    Its methods are executed before loading or after dumping a JSON string respectively.
-    """
-
-    @pre_load
-    def to_snakecase(self, data, **kwargs):
-        return {snakecase(key): value for key, value in data.items()}
-
-    @post_dump
-    def to_camelcase(self, data, **kwargs):
-        return {camelcase(key): value for key, value in data.items()}
diff --git a/src/bo4e/com/adresse.py b/src/bo4e/com/adresse.py
index 5119c9d36..fcccea202 100644
--- a/src/bo4e/com/adresse.py
+++ b/src/bo4e/com/adresse.py
@@ -1,27 +1,35 @@
-import attr
+"""
+Contains Adresse class
+and corresponding marshmallow schema for de-/serialization
+"""
 
-from marshmallow import Schema, fields, post_load
+import attr
+from marshmallow import fields, post_load
 from marshmallow_enum import EnumField
-
-from bo4e.cases import JavaScriptMixin
-from bo4e.com.com import COM
+from bo4e.com.com import COM, COMSchema
 from bo4e.enum.landescode import Landescode
 
 
+# pylint: disable=unused-argument
 def strasse_xor_postfach(instance, attribute, value):
+    """
+    An address is valid if it contains a postfach XOR (a strasse AND hausnummer).
+    This functions checks for these conditions of a valid address.
+    """
     if instance.strasse or instance.hausnummer:
         if instance.postfach:
             raise ValueError("Enter either strasse and hausnummer OR postfach.")
-        elif not instance.strasse:
+        if not instance.strasse:
             raise ValueError("Missing strasse to hausnummer.")
-        elif not instance.hausnummer:
+        if not instance.hausnummer:
             raise ValueError("Missing hausnummer to strasse.")
 
 
+# pylint: disable=too-many-instance-attributes, too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class Adresse(COM):
     """
-    Enthält eine Adresse, die für die meisten Zwecke verwendbar ist.
+    Contains an address that can be used for most purposes.
     """
 
     # required attributes
@@ -37,7 +45,11 @@ class Adresse(COM):
     landescode: Landescode = attr.ib(default=Landescode.DE)
 
 
-class AdresseSchema(Schema, JavaScriptMixin):
+class AdresseSchema(COMSchema):
+    """
+    Schema for de-/serialization of Adresse.
+    """
+
     # required attributes
     postleitzahl = fields.Str()
     ort = fields.Str()
@@ -50,6 +62,8 @@ class AdresseSchema(Schema, JavaScriptMixin):
     co_ergaenzung = fields.Str(missing=None)
     landescode = EnumField(Landescode)
 
+    # pylint: disable=no-self-use
     @post_load
-    def deserialise(self, data, **kwargs) -> Adresse:
+    def deserialize(self, data, **kwargs) -> Adresse:
+        """Deserialize JSON to Adresse object"""
         return Adresse(**data)
diff --git a/src/bo4e/com/com.py b/src/bo4e/com/com.py
index 539c48946..59c8397cd 100644
--- a/src/bo4e/com/com.py
+++ b/src/bo4e/com/com.py
@@ -1,8 +1,19 @@
+"""
+Contains base class for all components
+"""
 import attr
+from bo4e.schemata.caseconverterschema import CaseConverterSchema
 
-
+# pylint: disable=too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class COM:
     """
-    abstract base class for all components
+    base class for all components
+    """
+
+
+class COMSchema(CaseConverterSchema):
+    """
+    This is a base class.
+    All components objects schemata are inherited from this class.
     """
diff --git a/src/bo4e/com/dienstleistung.py b/src/bo4e/com/dienstleistung.py
new file mode 100644
index 000000000..5ecb3778f
--- /dev/null
+++ b/src/bo4e/com/dienstleistung.py
@@ -0,0 +1,37 @@
+"""
+Contains Dienstleistung class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+from bo4e.com.com import COM, COMSchema
+from bo4e.enum.dienstleistungstyp import Dienstleistungstyp
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Dienstleistung(COM):
+    """
+    Abbildung einer abrechenbaren Dienstleistung.
+    """
+
+    # required attributes
+    dienstleistungstyp: Dienstleistungstyp = attr.ib(validator=attr.validators.in_(Dienstleistungstyp))
+    bezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+
+
+class DienstleistungSchema(COMSchema):
+    """
+    Schema for de-/serialization of Dienstleistung.
+    """
+
+    # required attributes
+    dienstleistungstyp = EnumField(Dienstleistungstyp)
+    bezeichnung = fields.Str()
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Dienstleistung:
+        """Deserialize JSON to Dienstleistung object"""
+        return Dienstleistung(**data)
diff --git a/src/bo4e/com/externereferenz.py b/src/bo4e/com/externereferenz.py
index 7d457bb7c..68037822c 100644
--- a/src/bo4e/com/externereferenz.py
+++ b/src/bo4e/com/externereferenz.py
@@ -1,14 +1,19 @@
+"""
+Contains ExterenzeReferenz class
+and corresponding marshmallow schema for de-/serialization
+"""
 import attr
-from marshmallow import Schema, fields, post_load
-
-from bo4e.cases import JavaScriptMixin
-from bo4e.com.com import COM
+from marshmallow import fields, post_load
+from bo4e.com.com import COM, COMSchema
 
 
+# pylint: disable=too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class ExterneReferenz(COM):
     """
-    Viele Datenobjekte weisen in unterschiedlichen Systemen eine eindeutige ID (Kundennummer, GP-Nummer etc.) auf. Beim Austausch von Datenobjekten zwischen verschiedenen Systemen ist es daher hilfreich, sich die eindeutigen IDs der anzubindenden Systeme zu merken.
+    Viele Datenobjekte weisen in unterschiedlichen Systemen eine eindeutige ID (Kundennummer, GP-Nummer etc.) auf.
+    Beim Austausch von Datenobjekten zwischen verschiedenen Systemen ist es daher hilfreich,
+    sich die eindeutigen IDs der anzubindenden Systeme zu merken.
     """
 
     # required attributes
@@ -16,10 +21,17 @@ class ExterneReferenz(COM):
     ex_ref_wert: str
 
 
-class ExterneReferenzSchema(Schema, JavaScriptMixin):
+class ExterneReferenzSchema(COMSchema):
+    """
+    Schema for de-/serialization of ExterneReferenz.
+    Inherits from Schema and JavaScriptMixin.
+    """
+
     ex_ref_name = fields.Str()
     ex_ref_wert = fields.Str()
 
+    # pylint: disable=no-self-use, unused-argument
     @post_load
-    def deserialise(self, data, **kwargs) -> ExterneReferenz:
+    def deserialize(self, data, **kwargs) -> ExterneReferenz:
+        """Deserialize JSON to ExterneReferenz object"""
         return ExterneReferenz(**data)
diff --git a/src/bo4e/com/geokoordinaten.py b/src/bo4e/com/geokoordinaten.py
index 9d8bc2aa7..31193180b 100644
--- a/src/bo4e/com/geokoordinaten.py
+++ b/src/bo4e/com/geokoordinaten.py
@@ -1,12 +1,16 @@
-import attr
-
+"""
+Contains Geokoordinaten class
+and corresponding marshmallow schema for de-/serialization
+"""
 from decimal import Decimal
-from marshmallow import Schema, fields, post_load
 
-from bo4e.cases import JavaScriptMixin
-from bo4e.com.com import COM
+import attr
+from marshmallow import fields, post_load
 
+from bo4e.com.com import COM, COMSchema
 
+
+# pylint: disable=too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class Geokoordinaten(COM):
     """
@@ -18,10 +22,12 @@ class Geokoordinaten(COM):
     laengengrad: Decimal = attr.ib(validator=attr.validators.instance_of(Decimal))
 
 
-class GeokoordinatenSchema(Schema, JavaScriptMixin):
+class GeokoordinatenSchema(COMSchema):
     """
-    Standard json library can not serialise Decimal type.
-    Therefore these information will be serialised as string.
+    Schema for de-/serialization of Geokoordinaten.
+
+    Standard json library can not serialize Decimal type.
+    Therefore these information will be serialized as string.
     During the deserialisiation it will become a Decimal type again.
     Link to official documentain:
     https://marshmallow.readthedocs.io/en/latest/api_reference.html?highlight=function#marshmallow.fields.Decimal
@@ -30,6 +36,8 @@ class GeokoordinatenSchema(Schema, JavaScriptMixin):
     breitengrad = fields.Decimal(as_string=True)
     laengengrad = fields.Decimal(as_string=True)
 
+    # pylint: disable=no-self-use, unused-argument
     @post_load
-    def deserialise(self, data, **kwargs) -> Geokoordinaten:
+    def deserialize(self, data, **kwargs) -> Geokoordinaten:
+        """Deserialize JSON to Geokoordinaten object"""
         return Geokoordinaten(**data)
diff --git a/src/bo4e/com/hardware.py b/src/bo4e/com/hardware.py
new file mode 100644
index 000000000..296982c16
--- /dev/null
+++ b/src/bo4e/com/hardware.py
@@ -0,0 +1,37 @@
+"""
+Contains Hardware class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+from bo4e.com.com import COM, COMSchema
+from bo4e.enum.geraetetyp import Geraetetyp
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Hardware(COM):
+    """
+    Abbildung einer abrechenbaren Hardware.
+    """
+
+    # required attributes
+    geraetetyp: Geraetetyp = attr.ib(validator=attr.validators.in_(Geraetetyp))
+    bezeichnung: str = attr.ib(validator=attr.validators.instance_of(str))
+
+
+class HardwareSchema(COMSchema):
+    """
+    Schema for de-/serialization of Hardware.
+    """
+
+    # required attributes
+    geraetetyp = EnumField(Geraetetyp)
+    bezeichnung = fields.Str()
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Hardware:
+        """Deserialize JSON to Hardware object"""
+        return Hardware(**data)
diff --git a/src/bo4e/com/katasteradresse.py b/src/bo4e/com/katasteradresse.py
index 17925208f..0ac480d04 100644
--- a/src/bo4e/com/katasteradresse.py
+++ b/src/bo4e/com/katasteradresse.py
@@ -1,11 +1,13 @@
+"""
+Contains Katasteradresse class
+and corresponding marshmallow schema for de-/serialization
+"""
 import attr
-
-from marshmallow import Schema, fields, post_load
-
-from bo4e.cases import JavaScriptMixin
-from bo4e.com.com import COM
+from marshmallow import fields, post_load
+from bo4e.com.com import COM, COMSchema
 
 
+# pylint: disable=too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class Katasteradresse(COM):
     """
@@ -16,10 +18,16 @@ class Katasteradresse(COM):
     flurstueck: str
 
 
-class KatasteradresseSchema(Schema, JavaScriptMixin):
+class KatasteradresseSchema(COMSchema):
+    """
+    Schema for de-/serialization of Katasteradresse.
+    """
+
     gemarkung_flur = fields.Str()
     flurstueck = fields.Str()
 
+    # pylint: disable=no-self-use, unused-argument
     @post_load
-    def deserialise(self, data, **kwargs) -> Katasteradresse:
+    def deserialize(self, data, **kwargs) -> Katasteradresse:
+        """Deserialize JSON to Katasteradresse object"""
         return Katasteradresse(**data)
diff --git a/src/bo4e/com/menge.py b/src/bo4e/com/menge.py
new file mode 100644
index 000000000..00792fe25
--- /dev/null
+++ b/src/bo4e/com/menge.py
@@ -0,0 +1,40 @@
+"""
+Contains Menge class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+from decimal import Decimal
+
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+
+from bo4e.com.com import COM, COMSchema
+from bo4e.enum.mengeneinheit import Mengeneinheit
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Menge(COM):
+    """
+    Abbildung einer Menge mit Wert und Einheit.
+    """
+
+    # required attributes
+    wert: Decimal = attr.ib(validator=attr.validators.instance_of(Decimal))
+    einheit: Mengeneinheit = attr.ib(validator=attr.validators.instance_of(Mengeneinheit))
+
+
+class MengeSchema(COMSchema):
+    """
+    Schema for de-/serialization of Menge.
+    """
+
+    # required attributes
+    wert = fields.Decimal(as_string=True)
+    einheit = EnumField(Mengeneinheit)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Menge:
+        """Deserialize JSON to Menge object"""
+        return Menge(**data)
diff --git a/src/bo4e/com/messlokationszuordnung.py b/src/bo4e/com/messlokationszuordnung.py
index 62a320f1d..e9514abac 100644
--- a/src/bo4e/com/messlokationszuordnung.py
+++ b/src/bo4e/com/messlokationszuordnung.py
@@ -1,14 +1,17 @@
-import attr
-
+"""
+Contains Messlokationszuordnung class
+and corresponding marshmallow schema for de-/serialization
+"""
 from datetime import datetime
-from marshmallow import Schema, fields, post_load
-from marshmallow_enum import EnumField
 
-from bo4e.cases import JavaScriptMixin
-from bo4e.com.com import COM
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+from bo4e.com.com import COM, COMSchema
 from bo4e.enum.arithmetische_operation import ArithmetischeOperation
 
 
+# pylint: disable=too-few-public-methods
 @attr.s(auto_attribs=True, kw_only=True)
 class Messlokationszuordnung(COM):
     """
@@ -26,7 +29,11 @@ class Messlokationszuordnung(COM):
     gueltig_bis: datetime = attr.ib(default=None)
 
 
-class MesslokationszuordnungSchema(Schema, JavaScriptMixin):
+class MesslokationszuordnungSchema(COMSchema):
+    """
+    Schema for de-/serialization of Katasteradresse.
+    """
+
     # required attributes
     messlokations_id = fields.Str()
     arithmetik = EnumField(ArithmetischeOperation)
@@ -35,6 +42,8 @@ class MesslokationszuordnungSchema(Schema, JavaScriptMixin):
     gueltig_seit = fields.DateTime(missing=None)
     gueltig_bis = fields.DateTime(missing=None)
 
+    # pylint: disable=no-self-use, unused-argument
     @post_load
-    def deserialise(self, data, **kwargs) -> Messlokationszuordnung:
+    def deserialize(self, data, **kwargs) -> Messlokationszuordnung:
+        """Deserialize JSON to Messlokationszuordnung object"""
         return Messlokationszuordnung(**data)
diff --git a/src/bo4e/com/rufnummer.py b/src/bo4e/com/rufnummer.py
new file mode 100644
index 000000000..a8ab61a6f
--- /dev/null
+++ b/src/bo4e/com/rufnummer.py
@@ -0,0 +1,37 @@
+"""
+Contains Rufnummer class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+from bo4e.com.com import COM, COMSchema
+from bo4e.enum.rufnummernart import Rufnummernart
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Rufnummer(COM):
+    """
+    Contains information to call or fax someone
+    """
+
+    # required attributes
+    nummerntyp: Rufnummernart = attr.ib(validator=attr.validators.in_(Rufnummernart))
+    rufnummer: str = attr.ib(validator=attr.validators.instance_of(str))
+
+
+class RufnummerSchema(COMSchema):
+    """
+    Schema for de-/serialization of Rufnummer.
+    """
+
+    # required attributes
+    nummerntyp = EnumField(Rufnummernart)
+    rufnummer = fields.Str()
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Rufnummer:
+        """Deserialize JSON to Rufnummer object"""
+        return Rufnummer(**data)
diff --git a/src/bo4e/com/unterschrift.py b/src/bo4e/com/unterschrift.py
new file mode 100644
index 000000000..865cbcc2c
--- /dev/null
+++ b/src/bo4e/com/unterschrift.py
@@ -0,0 +1,41 @@
+"""
+Contains Unterschrift class
+and corresponding marshmallow schema for de-/serialization
+"""
+from datetime import datetime
+import attr
+from marshmallow import fields, post_load
+from bo4e.com.com import COM, COMSchema
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Unterschrift(COM):
+    """
+    Modellierung einer Unterschrift, z.B. für Verträge, Angebote etc.
+    """
+
+    # required attributes
+    name: str = attr.ib(validator=attr.validators.instance_of(str))
+
+    # optional attributes
+    ort: str = attr.ib(default=None)
+    datum: datetime = attr.ib(default=None)
+
+
+class UnterschriftSchema(COMSchema):
+    """
+    Schema for de-/serialization of Unterschrift.
+    """
+
+    # required attributes
+    name = fields.String()
+
+    # optional attributes
+    ort = fields.String(missing=None)
+    datum = fields.DateTime(missing=None)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Unterschrift:
+        """Deserialize JSON to Unterschrift object"""
+        return Unterschrift(**data)
diff --git a/src/bo4e/com/vertragskonditionen.py b/src/bo4e/com/vertragskonditionen.py
new file mode 100644
index 000000000..eac11af48
--- /dev/null
+++ b/src/bo4e/com/vertragskonditionen.py
@@ -0,0 +1,44 @@
+"""
+Contains Vertragskonditionen class
+and corresponding marshmallow schema for de-/serialization
+"""
+from decimal import Decimal
+import attr
+from marshmallow import fields, post_load
+from bo4e.com.com import COM, COMSchema
+from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Vertragskonditionen(COM):
+    """
+    Abbildung für Vertragskonditionen. Die Komponente wird sowohl im Vertrag als auch im Tarif verwendet.
+    """
+
+    # optional attributes
+    beschreibung: str = attr.ib(default=None)
+    anzahl_abschlaege: Decimal = attr.ib(default=None)
+    vertragslaufzeit: Zeitraum = attr.ib(default=None)
+    kuendigungsfrist: Zeitraum = attr.ib(default=None)
+    vertragsverlaengerung: Zeitraum = attr.ib(default=None)
+    abschlagszyklus: Zeitraum = attr.ib(default=None)
+
+
+class VertragskonditionenSchema(COMSchema):
+    """
+    Schema for de-/serialization of Vertragskonditionen.
+    """
+
+    # optional attributes
+    beschreibung = fields.String(missing=None)
+    anzahl_abschlaege = fields.Decimal(missing=None, as_string=True)
+    vertragslaufzeit = fields.Nested(ZeitraumSchema, missing=None)
+    kuendigungsfrist = fields.Nested(ZeitraumSchema, missing=None)
+    vertragsverlaengerung = fields.Nested(ZeitraumSchema, missing=None)
+    abschlagszyklus = fields.Nested(ZeitraumSchema, missing=None)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Vertragskonditionen:
+        """Deserialize JSON to Vertragskonditionen object"""
+        return Vertragskonditionen(**data)
diff --git a/src/bo4e/com/vertragsteil.py b/src/bo4e/com/vertragsteil.py
new file mode 100644
index 000000000..6e689a934
--- /dev/null
+++ b/src/bo4e/com/vertragsteil.py
@@ -0,0 +1,50 @@
+"""
+Contains Vertragsteil class
+and corresponding marshmallow schema for de-/serialization
+"""
+from typing import Optional
+from datetime import datetime
+import attr
+from marshmallow import fields, post_load
+from bo4e.com.com import COM, COMSchema
+from bo4e.com.menge import Menge, MengeSchema
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Vertragsteil(COM):
+    """
+    Abbildung für einen Vertragsteil. Der Vertragsteil wird dazu verwendet,
+    eine vertragliche Leistung in Bezug zu einer Lokation (Markt- oder Messlokation) festzulegen.
+    """
+
+    # required attributes
+    vertragsteilbeginn: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
+    vertragsteilende: datetime = attr.ib(validator=attr.validators.instance_of(datetime))
+
+    # optional attributes
+    lokation: Optional[str] = attr.ib(default=None)
+    vertraglich_fixierte_menge: Optional[Menge] = attr.ib(default=None)
+    minimale_abnahmemenge: Optional[Menge] = attr.ib(default=None)
+    maximale_abnahmemenge: Optional[Menge] = attr.ib(default=None)
+
+
+class VertragsteilSchema(COMSchema):
+    """
+    Schema for de-/serialization of Vertragsteil.
+    """
+
+    # required attributes
+    vertragsteilbeginn = fields.DateTime()
+    vertragsteilende = fields.DateTime()
+
+    # optional attributes
+    lokation = fields.String(missing=None)
+    vertraglich_fixierte_menge = fields.Nested(MengeSchema, missing=None)
+    minimale_abnahmemenge = fields.Nested(MengeSchema, missing=None)
+    maximale_abnahmemenge = fields.Nested(MengeSchema, missing=None)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Vertragsteil:
+        """Deserialize JSON to Vertragsteil object"""
+        return Vertragsteil(**data)
diff --git a/src/bo4e/com/zaehlwerk.py b/src/bo4e/com/zaehlwerk.py
new file mode 100644
index 000000000..bdf787446
--- /dev/null
+++ b/src/bo4e/com/zaehlwerk.py
@@ -0,0 +1,62 @@
+"""
+Contains Zaehlwerk class
+and corresponding marshmallow schema for de-/serialization
+"""
+from decimal import Decimal
+
+import attr
+from attr.validators import matches_re
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+
+from bo4e.com.com import COM, COMSchema
+from bo4e.enum.energierichtung import Energierichtung
+from bo4e.enum.mengeneinheit import Mengeneinheit
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Zaehlwerk(COM):
+    """
+    Mit dieser Komponente werden Zählwerke modelliert.
+    """
+
+    zaehlwerk_id: str = attr.ib(
+        validator=attr.validators.instance_of(str)
+    )  # Identifikation des Zählwerks (Registers) innerhalb des Zählers.
+    # Oftmals eine laufende Nummer hinter der Zählernummer. Z.B. 47110815_1
+    bezeichnung: str = attr.ib(
+        validator=attr.validators.instance_of(str)
+    )  # Zusätzliche Bezeichnung, z.B. Zählwerk_Wirkarbeit.
+    richtung: Energierichtung  # Die Energierichtung, Einspeisung oder Ausspeisung.
+    obis_kennzahl: str = attr.ib(
+        validator=matches_re(
+            r"(?:(1)-((?:[0-5]?[0-9])|(?:6[0-5])):((?:[1-8]|99))\.((?:6|8|9|29))\.([0-9]{1,2}))|"
+            r"(?:(7)-((?:[0-5]?[0-9])|(?:6[0-5])):(.{1,2})\.(.{1,2})\.([0-9]{1,2}))"
+        )
+    )  # Die OBIS-Kennzahl für das Zählwerk, die festlegt, welche auf die gemessene Größe mit dem Stand gemeldet wird.
+    # Nur Zählwerkstände mit dieser OBIS-Kennzahl werden an diesem Zählwerk registriert.
+    wandlerfaktor: Decimal = attr.ib(
+        validator=attr.validators.instance_of(Decimal)
+    )  # Mit diesem Faktor wird eine Zählerstandsdifferenz multipliziert, um zum eigentlichen Verbrauch im Zeitraum
+    # zu kommen.
+    einheit: Mengeneinheit  # Die Einheit der gemessenen Größe, z.B. kWh
+
+
+class ZaehlwerkSchema(COMSchema):
+    """
+    Schema for de-/serialization of Zaehlwerk.
+    """
+
+    zaehlwerk_id = fields.Str()
+    bezeichnung = fields.Str()
+    richtung = EnumField(Energierichtung)
+    obis_kennzahl = fields.Str()
+    wandlerfaktor = fields.Decimal(as_string=True)
+    einheit = EnumField(Mengeneinheit)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Zaehlwerk:
+        """Deserialize JSON to Zaehlwerk object"""
+        return Zaehlwerk(**data)
diff --git a/src/bo4e/com/zeitraum.py b/src/bo4e/com/zeitraum.py
new file mode 100644
index 000000000..ecef8f67f
--- /dev/null
+++ b/src/bo4e/com/zeitraum.py
@@ -0,0 +1,89 @@
+"""
+Contains Zeitraum class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+from typing import Optional
+from decimal import Decimal
+from datetime import datetime
+
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+
+from bo4e.com.com import COM, COMSchema
+from bo4e.enum.zeiteinheit import Zeiteinheit
+
+# pylint: disable=unused-argument
+def time_range_possibilities(instance, attribute, value):
+    """
+    An address is valid if it contains a postfach XOR (a strasse AND hausnummer).
+    This functions checks for these conditions of a valid address.
+    """
+    if (
+        instance.einheit
+        and instance.dauer
+        and not (instance.startdatum or instance.enddatum or instance.startzeitpunkt or instance.endzeitpunkt)
+    ):
+        return
+    if (
+        instance.startdatum
+        and instance.enddatum
+        and not (instance.einheit or instance.dauer or instance.startzeitpunkt or instance.endzeitpunkt)
+    ):
+        return
+    if (
+        instance.startzeitpunkt
+        and instance.endzeitpunkt
+        and not (instance.einheit or instance.dauer or instance.startdatum or instance.enddatum)
+    ):
+        return
+
+    raise ValueError(
+        """
+        Please choose from one of the three possibilities to specify the timerange:
+        - einheit and dauer
+        - startdatum and enddatum
+        - startzeitpunkt and endzeitpunkt
+        """
+    )
+
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Zeitraum(COM):
+    """
+    Diese Komponente wird zur Abbildung von Zeiträumen in Form von Dauern oder der Angabe von Start und Ende verwendet.
+    Es muss daher eine der drei Möglichkeiten angegeben sein:
+    - Einheit und Dauer oder
+    - Zeitraum: Startdatum bis Enddatum oder
+    - Zeitraum: Startzeitpunkt (Datum und Uhrzeit) bis Endzeitpunkt (Datum und Uhrzeit)
+    """
+
+    # optional attributes
+    einheit: Optional[Zeiteinheit] = attr.ib(default=None, validator=time_range_possibilities)
+    dauer: Optional[Decimal] = attr.ib(default=None, validator=time_range_possibilities)
+    startdatum: Optional[datetime] = attr.ib(default=None, validator=time_range_possibilities)
+    enddatum: Optional[datetime] = attr.ib(default=None, validator=time_range_possibilities)
+    startzeitpunkt: Optional[datetime] = attr.ib(default=None, validator=time_range_possibilities)
+    endzeitpunkt: Optional[datetime] = attr.ib(default=None, validator=time_range_possibilities)
+
+
+class ZeitraumSchema(COMSchema):
+    """
+    Schema for de-/serialization of Zeitraum.
+    """
+
+    # optional attributes
+    einheit = EnumField(Zeiteinheit, missing=None)
+    dauer = fields.Decimal(missing=None, as_string=True)
+    startdatum = fields.DateTime(missing=None)
+    enddatum = fields.DateTime(missing=None)
+    startzeitpunkt = fields.DateTime(missing=None)
+    endzeitpunkt = fields.DateTime(missing=None)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Zeitraum:
+        """Deserialize JSON to Zeitraum object"""
+        return Zeitraum(**data)
diff --git a/src/bo4e/com/zustaendigkeit.py b/src/bo4e/com/zustaendigkeit.py
new file mode 100644
index 000000000..c64a86079
--- /dev/null
+++ b/src/bo4e/com/zustaendigkeit.py
@@ -0,0 +1,43 @@
+"""
+Contains Zustaendigkeit class
+and corresponding marshmallow schema for de-/serialization
+"""
+
+import attr
+from marshmallow import fields, post_load
+from marshmallow_enum import EnumField
+from bo4e.com.com import COM, COMSchema
+from bo4e.enum.themengebiet import Themengebiet
+
+# pylint: disable=too-few-public-methods
+@attr.s(auto_attribs=True, kw_only=True)
+class Zustaendigkeit(COM):
+    """
+    Enthält die zeitliche Zuordnung eines Ansprechpartners zu Abteilungen und Zuständigkeiten.
+    """
+
+    # required attributes
+    themengebiet: Themengebiet = attr.ib(validator=attr.validators.in_(Themengebiet))
+
+    # optional attributes
+    jobtitel: str = attr.ib(default=None)
+    abteilung: str = attr.ib(default=None)
+
+
+class ZustaendigkeitSchema(COMSchema):
+    """
+    Schema for de-/serialization of Zustaendigkeit.
+    """
+
+    # required attributes
+    themengebiet = EnumField(Themengebiet)
+
+    # optional attributes
+    jobtitel: str = fields.Str(missing=None)
+    abteilung: str = fields.Str(missing=None)
+
+    # pylint: disable=no-self-use, unused-argument
+    @post_load
+    def deserialize(self, data, **kwargs) -> Zustaendigkeit:
+        """Deserialize JSON to Zustaendigkeit object"""
+        return Zustaendigkeit(**data)
diff --git a/src/bo4e/enum/botyp.py b/src/bo4e/enum/botyp.py
index e01754cdb..fb6063df1 100644
--- a/src/bo4e/enum/botyp.py
+++ b/src/bo4e/enum/botyp.py
@@ -30,6 +30,7 @@
     "TARIFINFO": "TARIFINFO",
     "TARIFKOSTEN": "TARIFKOSTEN",
     "TARIFPREISBLATT": "TARIFPREISBLATT",
+    "VERTRAG": "VERTRAG",
     "ZAEHLER": "ZAEHLER",
     "ZEITREIHE": "ZEITREIHE",
 }
diff --git a/src/bo4e/enum/dienstleistungstyp.py b/src/bo4e/enum/dienstleistungstyp.py
new file mode 100644
index 000000000..cd62eeb7d
--- /dev/null
+++ b/src/bo4e/enum/dienstleistungstyp.py
@@ -0,0 +1,44 @@
+"""
+Auflistung möglicher abzurechnender Dienstleistungen.
+"""
+from enum import Enum
+
+# pylint: disable=line-too-long
+_dienstleistungstyp = {
+    "DATENBEREITSTELLUNG_TAEGLICH": "DATENBEREITSTELLUNG_TAEGLICH",  # Datenbereitstellung täglich
+    "DATENBEREITSTELLUNG_WOECHENTLICH": "DATENBEREITSTELLUNG_WOECHENTLICH",  # Datenbereitstellung wöchentlich
+    "DATENBEREITSTELLUNG_MONATLICH": "DATENBEREITSTELLUNG_MONATLICH",  # Datenbereitstellung monatlich
+    "DATENBEREITSTELLUNG_JAEHRLICH": "DATENBEREITSTELLUNG_JAEHRLICH",  # Datenbereitstellung jährlich
+    "DATENBEREITSTELLUNG_HISTORISCHE_LG": "DATENBEREITSTELLUNG_HISTORISCHE_LG",  # Datenbereitstellung historischer Lastgänge
+    "DATENBEREITSTELLUNG_STUENDLICH": "DATENBEREITSTELLUNG_STUENDLICH",  # Datenbereitstellung stündlich
+    "DATENBEREITSTELLUNG_VIERTELJAEHRLICH": "DATENBEREITSTELLUNG_VIERTELJAEHRLICH",  # Datenbereitstellung vierteljährlich
+    "DATENBEREITSTELLUNG_HALBJAEHRLICH": "DATENBEREITSTELLUNG_HALBJAEHRLICH",  # Datenbereitstellung halbjährlich
+    "DATENBEREITSTELLUNG_MONATLICH_ZUSAETZLICH": "DATENBEREITSTELLUNG_MONATLICH_ZUSAETZLICH",  # Datenbereitstellung monatlich zusätzlich
+    "DATENBEREITSTELLUNG_EINMALIG": "DATENBEREITSTELLUNG_EINMALIG",  # Datenbereitstellung einmalig
+    "AUSLESUNG_2X_TAEGLICH_FERNAUSLESUNG": "AUSLESUNG_2X_TAEGLICH_FERNAUSLESUNG",  # Auslesung 2x täglich mittels Fernauslesung
+    "AUSLESUNG_TAEGLICH_FERNAUSLESUNG": "AUSLESUNG_TAEGLICH_FERNAUSLESUNG",  # Auslesung täglich mittels Fernauslesung
+    "AUSLESUNG_MANUELL_MSB": "AUSLESUNG_MANUELL_MSB",  # Auslesung, manuell vom Messstellenbetreiber vorgenommen
+    "AUSLESUNG_MONATLICH_FERNAUSLESUNG": "AUSLESUNG_MONATLICH_FERNAUSLESUNG",  # Auslesung monatlich bei mittels Fernauslesung
+    "AUSLESUNG_JAEHRLICH_FERNAUSLESUNG": "AUSLESUNG_JAEHRLICH_FERNAUSLESUNG",  # Auslesung jährlich bei SLP mittels Fernauslesung
+    "AUSLESUNG_MDE": "AUSLESUNG_MDE",  # Auslesung mit mobiler Daten Erfassung (MDE)
+    "ABLESUNG_MONATLICH": "ABLESUNG_MONATLICH",  # Ablesung monatlich
+    "ABLESUNG_VIERTELJAEHRLICH": "ABLESUNG_VIERTELJAEHRLICH",  # Ablesung vierteljährlich
+    "ABLESUNG_HALBJAEHRLICH": "ABLESUNG_HALBJAEHRLICH",  # Ablesung halbjährlich
+    "ABLESUNG_JAEHRLICH": "ABLESUNG_JAEHRLICH",  # Ablesung jährlich
+    "AUSLESUNG_FERNAUSLESUNG": "AUSLESUNG_FERNAUSLESUNG",  # Auslesung mittels Fernauslesung
+    "ABLESUNG_ZUSAETZLICH_MSB": "ABLESUNG_ZUSAETZLICH_MSB",  # Ablesung, zusätzlich vom Messstellenbetreiber vorgenommen
+    "ABLESUNG_ZUSAETZLICH_KUNDE": "ABLESUNG_ZUSAETZLICH_KUNDE",  # Ablesung SLP, zusätzlich vom Kunden vorgenommen
+    "AUSLESUNG_FERNAUSLESUNG_ZUSAETZLICH_MSB": "AUSLESUNG_FERNAUSLESUNG_ZUSAETZLICH_MSB",  # Auslesung, mittels Fernauslesung, zusätzlich vom Messstellenbetreiber vorgenommen
+    "AUSLESUNG_MOATLICH_FERNAUSLESUNG": "AUSLESUNG_MOATLICH_FERNAUSLESUNG",  # Auslesung monatlich mittels Fernauslesung
+    "AUSLESUNG_STUENDLICH_FERNAUSLESUNG": "AUSLESUNG_STUENDLICH_FERNAUSLESUNG",  # Auslesung stündlich mittels Fernauslesung
+    "AUSLESUNG_TEMPERATURMENGENUMWERTER": "AUSLESUNG_TEMPERATURMENGENUMWERTER",  # Auslesung Temperaturmengenumwerter
+    "AUSLESUNG_ZUSTANDSMENGENUMWERTER": "AUSLESUNG_ZUSTANDSMENGENUMWERTER",  # Auslesung Zustandsmengenumwerter
+    "AUSLESUNG_SYSTEMMENGENUMWERTER": "AUSLESUNG_SYSTEMMENGENUMWERTER",  # Auslesung Systemmengenumwerter
+    "AUSLESUNG_VORGANG": "AUSLESUNG_VORGANG",  # Auslesung je Vorgang
+    "AUSLESUNG_KOMPAKTMENGENUMWERTER": "AUSLESUNG_KOMPAKTMENGENUMWERTER",  # Auslesung Kompaktmengenumwerter
+    "SPERRUNG": "SPERRUNG",  # Sperrung einer Abnahmestelle
+    "ENTSPERRUNG": "ENTSPERRUNG",  # Entsperrung einer Abnahmestelle
+    "MAHNKOSTEN": "MAHNKOSTEN",  # Mahnkosten
+    "INKASSOKOSTEN": "INKASSOKOSTEN",  # Inkassokosten
+}
+Dienstleistungstyp: Enum = Enum("Dienstleistungstyp", _dienstleistungstyp)
diff --git a/src/bo4e/enum/geraetetyp.py b/src/bo4e/enum/geraetetyp.py
new file mode 100644
index 000000000..0e168fa63
--- /dev/null
+++ b/src/bo4e/enum/geraetetyp.py
@@ -0,0 +1,53 @@
+"""
+Auflistung möglicher abzurechnender Gerätetypen.
+"""
+from enum import Enum
+
+# pylint: disable=line-too-long
+_geraetetyp = {
+    "WECHSELSTROMZAEHLER": "WECHSELSTROMZAEHLER",  # Wechselstromzähler
+    "DREHSTROMZAEHLER": "DREHSTROMZAEHLER",  # Drehstromzähler
+    "ZWEIRICHTUNGSZAEHLER": "ZWEIRICHTUNGSZAEHLER",  # Zweirichtungszähler
+    "RLM_ZAEHLER": "RLM_ZAEHLER",  # RLM-Zähler
+    "BALGENGASZAEHLER": "BALGENGASZAEHLER",  # Balgengaszähler
+    "MAXIMUMZAEHLER": "MAXIMUMZAEHLER",  # Maximumzähler (Schleppzähler)
+    "MULTIPLEXANLAGE": "MULTIPLEXANLAGE",  # Multiplexeranlage
+    "PAUSCHALANLAGE": "PAUSCHALANLAGE",  # Pauschalanlagen
+    "VERSTAERKERANLAGE": "VERSTAERKERANLAGE",  # Verstärkeranlage
+    "SUMMATIONSGERAET": "SUMMATIONSGERAET",  # Summationsgerät
+    "IMPULSGEBER": "IMPULSGEBER",  # Impulsgeber
+    "EDL_21_ZAEHLERAUFSATZ": "EDL_21_ZAEHLERAUFSATZ",  # EDL 21 Zähleraufsatz für Zähler
+    "VIER_QUADRANTEN_LASTGANGZAEHLER": "VIER_QUADRANTEN_LASTGANGZAEHLER",  # Vier-Quadranten-Lastgangzähler
+    "MENGENUMWERTER": "MENGENUMWERTER",  # Mengenumwerter
+    "STROMWANDLER": "STROMWANDLER",  # Stromwandler
+    "SPANNUNGSWANDLER": "SPANNUNGSWANDLER",  # Spannungs-Wandler
+    "KOMBIMESSWANDLER": "KOMBIMESSWANDLER",  # Kombimesswandler
+    "BLOCKSTROMWANDLER": "BLOCKSTROMWANDLER",  # Blockstromwandler
+    "DATENLOGGER": "DATENLOGGER",  # Datenlogger
+    "KOMMUNIKATIONSANSCHLUSS": "KOMMUNIKATIONSANSCHLUSS",  # Kommunikationsanschluss
+    "MODEM": "MODEM",  # Modem
+    "TELEKOMMUNIKATIONSEINRICHTUNG": "TELEKOMMUNIKATIONSEINRICHTUNG",  # vom Messstellenbetreiber beigestellte Telekommunikationseinrichtung (Telefonanschluss)
+    "DREHKOLBENGASZAEHLER": "DREHKOLBENGASZAEHLER",  # Drehkolbengaszähler
+    "TURBINENRADGASZAEHLER": "TURBINENRADGASZAEHLER",  # Turbinenradgaszähler
+    "ULTRASCHALLZAEHLER": "ULTRASCHALLZAEHLER",  # Ultraschallzähler
+    "WIRBELGASZAEHLER": "WIRBELGASZAEHLER",  # Wirbelgaszähler
+    "MODERNE_MESSEINRICHTUNG": "MODERNE_MESSEINRICHTUNG",  # moderne Messeinrichtung
+    "INTELLIGENTES_MESSYSTEM": "INTELLIGENTES_MESSYSTEM",  # Intelligentes Messystem
+    "ELEKTRONISCHER_HAUSHALTSZAEHLER": "ELEKTRONISCHER_HAUSHALTSZAEHLER",  # elektronischer Haushaltszähler
+    "STEUEREINRICHTUNG": "STEUEREINRICHTUNG",  # Steuereinrichtung
+    "TARIFSCHALTGERAET": "TARIFSCHALTGERAET",  # Tarifschaltgerät
+    "RUNDSTEUEREMPFAENGER": "RUNDSTEUEREMPFAENGER",  # Rundsteuerempfänger
+    "OPTIONALE_ZUS_ZAEHLEINRICHTUNG": "OPTIONALE_ZUS_ZAEHLEINRICHTUNG",  # optionale zusätzliche Zähleinrichtung
+    "MESSWANDLERSATZ_IMS_MME": "MESSWANDLERSATZ_IMS_MME",  # Messwandlersatz Strom iMS und mME, NSP
+    "KOMBIMESSWANDLER_IMS_MME": "KOMBIMESSWANDLER_IMS_MME",  # Kombimesswandlersatz (Strom u. Spg) iMS und mME
+    "TARIFSCHALTGERAET_IMS_MME": "TARIFSCHALTGERAET_IMS_MME",  # Tarifschaltung iMS und mME
+    "RUNDSTEUEREMPFAENGER_IMS_MME": "RUNDSTEUEREMPFAENGER_IMS_MME",  # Rundsteuerempfänger iMS und mME
+    "TEMPERATUR_KOMPENSATION": "TEMPERATUR_KOMPENSATION",  # Temperaturkompensation
+    "HOECHSTBELASTUNGS_ANZEIGER": "HOECHSTBELASTUNGS_ANZEIGER",  # Höchsbelastungsanzeiger
+    "SONSTIGES_GERAET": "SONSTIGES_GERAET",  # Sonstiges Gerät
+    "PREPAYMENTZAEHLER": "PREPAYMENTZAEHLER",  # Prepaymentzähler
+    "EDL_21": "EDL_21",  # EDL21
+    "96_H_ZAEHLER": "96_H_ZAEHLER",  # 96 h Zähler
+    "EDL_40_ZAEHLERAUFSATZ": "EDL_40_ZAEHLERAUFSATZ",  # EDL 40 Zähleraufsatz für Zähler
+}
+Geraetetyp = Enum("Geraetetyp", _geraetetyp)
diff --git a/src/bo4e/enum/landescode.py b/src/bo4e/enum/landescode.py
index 075bbe281..f8a9fd5a5 100644
--- a/src/bo4e/enum/landescode.py
+++ b/src/bo4e/enum/landescode.py
@@ -2,6 +2,7 @@
 Der ISO-Landescode als Enumeration.
 """
 from enum import Enum
+
 from iso3166 import countries
 
 alpha2codes = {c.alpha2: c.alpha2 for c in countries}
diff --git a/src/bo4e/enum/marktrolle.py b/src/bo4e/enum/marktrolle.py
index c23effb65..9682e9cf0 100644
--- a/src/bo4e/enum/marktrolle.py
+++ b/src/bo4e/enum/marktrolle.py
@@ -17,5 +17,6 @@
     "RB": "RB",  # Registerbetreiber
     "KUNDE": "KUNDE",
     "INTERESSENT": "INTERESSENT",
+    "BTR": "BTR",  # Betreiber einer technischen Ressource
 }
 Marktrolle = Enum("Marktrolle", _marktrolle)
diff --git a/src/bo4e/enum/mengeneinheit.py b/src/bo4e/enum/mengeneinheit.py
new file mode 100644
index 000000000..1eb29136e
--- /dev/null
+++ b/src/bo4e/enum/mengeneinheit.py
@@ -0,0 +1,22 @@
+"""
+Einheit: Messgrößen, die per Messung oder Vorgabe ermittelt werden können.
+"""
+from enum import Enum
+
+_mengeneinheit = {
+    "W": "W",  # Watt
+    "WH": "WH",  # Wattstunde
+    "KW": "KW",  # Kilowatt
+    "KWH": "KWH",  # Kilowattstunde
+    "KVARH": "KVARH",  # Kilovarstunde
+    "MW": "MW",  # Megawatt
+    "MWH": "MWH",  # Megawattstunde
+    "STUECK": "STUECK",  # Stückzahl
+    "KUBIKMETER": "KUBIKMETER",  # Kubikmeter (Gas)
+    "STUNDE": "STUNDE",  # Stunde
+    "TAG": "TAG",  # Tage
+    "MONAT": "MONAT",  # Monat
+    "JAHR": "JAHR",  # Jahr
+    "PROZENT": "PROZENT",  # Prozent
+}
+Mengeneinheit = Enum("Mengeneinheit", _mengeneinheit)
diff --git a/src/bo4e/enum/rufnummernart.py b/src/bo4e/enum/rufnummernart.py
new file mode 100644
index 000000000..24733be44
--- /dev/null
+++ b/src/bo4e/enum/rufnummernart.py
@@ -0,0 +1,18 @@
+"""
+Abbildung verschiedener Rufnummerntypen.
+"""
+
+from enum import Enum
+
+_rufnummernart = {
+    "RUF_ZENTRALE": "RUF_ZENTRALE",  # Rufnummer Zentrale
+    "FAX_ZENTRALE": "FAX_ZENTRALE",  # Faxnummer Zentrale
+    "SAMMELRUF": "SAMMELRUF",  # Sammelrufnummer
+    "SAMMELFAX": "SAMMELFAX",  # Sammelfaxnummer
+    "ABTEILUNGRUF": "ABTEILUNGRUF",  # Rufnummer Abteilung
+    "ABTEILUNGFAX": "ABTEILUNGFAX",  # Faxnummer Abteilung
+    "RUF_DURCHWAHL": "RUF_DURCHWAHL",  # Rufnummer Durchwahl
+    "FAX_DURCHWAHL": "FAX_DURCHWAHL",  # Faxnummer Durchwahl
+    "MOBIL_NUMMER": "MOBIL_NUMMER",  # Nummer des mobilen Telefons
+}
+Rufnummernart = Enum("Rufnummernart", _rufnummernart)
diff --git a/src/bo4e/enum/tarifart.py b/src/bo4e/enum/tarifart.py
new file mode 100644
index 000000000..95d661932
--- /dev/null
+++ b/src/bo4e/enum/tarifart.py
@@ -0,0 +1,14 @@
+"""
+Die Tarifart wird verwendet zur Charakterisierung von Zählern und daraus resultierenden Tarifen.
+"""
+
+from enum import Enum
+
+_tarifart = {
+    "EINTARIF": "EINTARIF",  # Eintarif
+    "ZWEITARIF": "ZWEITARIF",  # Zweitarif
+    "MEHRTARIF": "MEHRTARIF",  # Mehrtarif
+    "SMART_METER": "SMART_METER",  # Smart Meter Tarif
+    "LEISTUNGSGEMESSEN": "LEISTUNGSGEMESSEN",  # Leistungsgemessener Tarif
+}
+Tarifart = Enum("Tarifart", _tarifart)
diff --git a/src/bo4e/enum/themengebiet.py b/src/bo4e/enum/themengebiet.py
new file mode 100644
index 000000000..b6dbbb386
--- /dev/null
+++ b/src/bo4e/enum/themengebiet.py
@@ -0,0 +1,71 @@
+"""
+Über dieses ENUM kann eine thematische Zuordnung, beispielsweise eines Ansprechpartners, vorgenommen werden.
+"""
+
+from enum import Enum
+
+
+_themengebiet = {
+    "ALLGEMEINER_INFORMATIONSAUSTAUSCH": "ALLGEMEINER_INFORMATIONSAUSTAUSCH",  # Allgemeiner Informationsaustausch
+    "AN_UND_ABMELDUNG": "AN_UND_ABMELDUNG",  # An- und Abmeldung
+    "ANSPRECHPARTNER_ALLGEMEIN": "ANSPRECHPARTNER_ALLGEMEIN",  # Ansprechpartner Allgemein
+    "ANSPRECHPARTNER_BDEW_DVGW": "ANSPRECHPARTNER_BDEW_DVGW",  # Ansprechpartner BDEW/DVGW
+    "ANSPRECHPARTNER_IT_TECHNIK": "ANSPRECHPARTNER_IT_TECHNIK",  # Ansprechpartner IT/Technik
+    "BILANZIERUNG": "BILANZIERUNG",  # Bilanzierung
+    "BILANZKREISKOORDINATOR": "BILANZKREISKOORDINATOR",  # Bilanzkreiskoordinator
+    "BILANZKREISVERANTWORTLICHER": "BILANZKREISVERANTWORTLICHER",  # Bilanzkreisverantwortlicher
+    "DATENFORMATE_ZERTIFIKATE_VERSCHLUESSELUNGEN": "DATENFORMATE_ZERTIFIKATE_VERSCHLUESSELUNGEN",
+    # Datenformate, Zertifikate, Verschlüsselungen
+    "DEBITORENMANAGEMENT": "DEBITORENMANAGEMENT",  # Debitorenmanagement
+    "DEMAND_SIDE_MANAGEMENT": "DEMAND_SIDE_MANAGEMENT",  # Demand-Side-Management
+    "EDI_VEREINBARUNG": "EDI_VEREINBARUNG",  # EDI-Vereinbarung
+    "EDIFACT": "EDIFACT",  # EDIFACT
+    "ENERGIEDATENMANAGEMENT": "ENERGIEDATENMANAGEMENT",  # Energiedatenmanagement
+    "FAHRPLANMANAGEMENT": "FAHRPLANMANAGEMENT",  # Fahrplanmanagement
+    "ALOCAT": "ALOCAT",  # Format:ALOCAT
+    "APERAK": "APERAK",  # Format:APERAK
+    "CONTRL": "CONTRL",  # Format:CONTRL
+    "INVOIC": "INVOIC",  # Format:INVOIC
+    "MSCONS": "MSCONS",  # Format:MSCONS
+    "ORDERS": "ORDERS",  # Format:ORDERS
+    "ORDERSP": "ORDERSP",  # Format:ORDERSP
+    "REMADV": "REMADV",  # Format:REMADV
+    "UTILMD": "UTILMD",  # Format:UTILMD
+    "GABI": "GABI",  # GaBi Gas
+    "GELI": "GELI",  # GeLi Gas
+    "GERAETERUECKGABE": "GERAETERUECKGABE",  # Geräterückgabe
+    "GERAETEWECHSEL": "GERAETEWECHSEL",  # Gerätewechsel
+    "GPKE": "GPKE",  # GPKE
+    "INBETRIEBNAHME": "INBETRIEBNAHME",  # Inbetriebnahme
+    "KAPAZITAETSMANAGEMENT": "KAPAZITAETSMANAGEMENT",  # Kapazitätsmanagement
+    "KLAERFAELLE": "KLAERFAELLE",  # Klärfälle
+    "LASTGAENGE_RLM": "LASTGAENGE_RLM",  # Lastgänge RLM
+    "LIEFERANTENRAHMENVERTRAG": "LIEFERANTENRAHMENVERTRAG",  # Lieferantenrahmenvertrag
+    "LIEFERANTENWECHSEL": "LIEFERANTENWECHSEL",  # Lieferantenwechsel
+    "MABIS": "MABIS",  # MaBiS
+    "MAHNWESEN": "MAHNWESEN",  # Mahnwesen
+    "MARKTGEBIETSVERANTWORTLICHER": "MARKTGEBIETSVERANTWORTLICHER",  # Marktgebietsverantwortlicher
+    "MARKTKOMMUNIKATION": "MARKTKOMMUNIKATION",  # Marktkommunikation
+    "MEHR_MINDERMENGEN": "MEHR_MINDERMENGEN",  # Mehr- Mindermengen
+    "MSB_MDL": "MSB_MDL",  # MSB - MDL
+    "NETZABRECHNUNG": "NETZABRECHNUNG",  # Netzabrechnung
+    "NETZENTGELTE": "NETZENTGELTE",  # Netzentgelte
+    "NETZMANAGEMENT": "NETZMANAGEMENT",  # Netzmanagement
+    "RECHT": "RECHT",  # Recht
+    "REGULIERUNGSMANAGEMENT": "REGULIERUNGSMANAGEMENT",  # Regulierungsmanagement
+    "REKLAMATIONEN": "REKLAMATIONEN",  # Reklamationen
+    "SPERREN_ENTSPERREN_INKASSO": "SPERREN_ENTSPERREN_INKASSO",  # Sperren/Entsperren/Inkasso
+    "STAMMDATEN": "STAMMDATEN",  # Stammdaten
+    "STOERUNGSFAELLE": "STOERUNGSFAELLE",  # Übermittlung_Störungsfälle
+    "TECHNISCHE_FRAGEN": "TECHNISCHE_FRAGEN",  # Technische Fragen
+    "UMSTELLUNG_INVOIC": "UMSTELLUNG_INVOIC",  # Umstellung INVOIC
+    "VERSCHLUESSELUNG_SIGNATUR": "VERSCHLUESSELUNG_SIGNATUR",  # Verschlüsselung/Signatur
+    "VERTRAGSMANAGEMENT": "VERTRAGSMANAGEMENT",  # Vertragsmanagement
+    "VERTRIEB": "VERTRIEB",  # Vertrieb
+    "WIM": "WIM",  # WiM
+    "ZAEHLERSTAENDE_SLP": "ZAEHLERSTAENDE_SLP",  # Zählerstände SLP
+    "ZAHLUNGSVERKEHR": "ZAHLUNGSVERKEHR",  # Zahlungsverkehr
+    "ZUORDNUNGSVEREINBARUNG": "ZUORDNUNGSVEREINBARUNG",  # Zuordnungsvereinbarung
+}
+
+Themengebiet = Enum("Themengebiet", _themengebiet)
diff --git a/src/bo4e/enum/titel.py b/src/bo4e/enum/titel.py
new file mode 100644
index 000000000..8fa6b92ed
--- /dev/null
+++ b/src/bo4e/enum/titel.py
@@ -0,0 +1,12 @@
+"""
+Übersicht möglicher Titel, z.B. eines Geschäftspartners.
+"""
+
+from enum import Enum
+
+_titel = {
+    "DR": "DR",  # Doktor
+    "PROF": "PROF",  # Professor
+    "PROF_DR": "PROF_DR",  # Professor Dr.
+}
+Titel = Enum("Titel", _titel)
diff --git a/src/bo4e/enum/vertragsart.py b/src/bo4e/enum/vertragsart.py
new file mode 100644
index 000000000..d96128459
--- /dev/null
+++ b/src/bo4e/enum/vertragsart.py
@@ -0,0 +1,13 @@
+"""
+Aufzählung der Vertragsarten.
+"""
+from enum import Enum
+
+_vertragsart = {
+    "ENERGIELIEFERVERTRAG": "ENERGIELIEFERVERTRAG",  # Energieliefervertrag
+    "NETZNUTZUNGSVERTRAG": "NETZNUTZUNGSVERTRAG",  # Netznutzungsvertrag
+    "BILANZIERUNGSVERTRAG": "BILANZIERUNGSVERTRAG",  # Bilanzierungsvertrag
+    "MESSSTELLENBETRIEBSVERTRAG": "MESSSTELLENBETRIEBSVERTRAG",  # Messstellenabetriebsvertrag
+    "BUENDELVERTRAG": "BUENDELVERTRAG",  # Bündelvertrag
+}
+Vertragsart = Enum("Vertragsart", _vertragsart)
diff --git a/src/bo4e/enum/vertragsstatus.py b/src/bo4e/enum/vertragsstatus.py
new file mode 100644
index 000000000..b6912241b
--- /dev/null
+++ b/src/bo4e/enum/vertragsstatus.py
@@ -0,0 +1,17 @@
+"""
+Abbildung einer Statusinformation für Verträge.
+"""
+from enum import Enum
+
+_vertragsstatus = {
+    "IN_ARBEIT": "IN_ARBEIT",  # in Arbeit
+    "UEBERMITTELT": "UEBERMITTELT",  # übermittelt
+    "ANGENOMMEN": "ANGENOMMEN",  # angenommen
+    "AKTIV": "AKTIV",  # aktiv
+    "ABGELEHNT": "ABGELEHNT",  # abgelehnt
+    "WIDERRUFEN": "WIDERRUFEN",  # widerrufen
+    "STORNIERT": "STORNIERT",  # storniert
+    "GEKUENDIGT": "GEKUENDIGT",  # gekündigt
+    "BEENDET": "BEENDET",  # beendet
+}
+Vertragsstatus = Enum("Vertragsstatus", _vertragsstatus)
diff --git a/src/bo4e/enum/zaehlerauspraegung.py b/src/bo4e/enum/zaehlerauspraegung.py
new file mode 100644
index 000000000..0594873aa
--- /dev/null
+++ b/src/bo4e/enum/zaehlerauspraegung.py
@@ -0,0 +1,11 @@
+"""
+Gibt an, ob es sich um einen Einrichtungs- oder Zweirichtungszähler handelt.
+"""
+
+from enum import Enum
+
+_zaehlerauspraegung = {
+    "EINRICHTUNGSZAEHLER": "EINRICHTUNGSZAEHLER",  # Einrichtungszaehler
+    "ZWEIRICHTUNGSZAEHLER": "ZWEIRICHTUNGSZAEHLER",  # Zweirichtungszaehler
+}
+Zaehlerauspraegung = Enum("Zaehlerauspraegung", _zaehlerauspraegung)
diff --git a/src/bo4e/enum/zaehlertyp.py b/src/bo4e/enum/zaehlertyp.py
new file mode 100644
index 000000000..e3c8e7cd5
--- /dev/null
+++ b/src/bo4e/enum/zaehlertyp.py
@@ -0,0 +1,18 @@
+"""
+Bei diesem Enum handelt es sich um die Abbildung von Zählertypen der Sparten Strom und Gas.
+"""
+
+from enum import Enum
+
+_zaehlertyp = {
+    "DREHSTROMZAEHLER": "DREHSTROMZAEHLER",  # Drehstromzähler
+    "BALGENGASZAEHLER": "BALGENGASZAEHLER",  # Balgengaszähler
+    "DREHKOLBENZAEHLER": "DREHKOLBENZAEHLER",  # Drehkolbengaszähler
+    "SMARTMETER": "SMARTMETER",  # Smart Meter Zähler
+    "LEISTUNGSZAEHLER": "LEISTUNGSZAEHLER",  # leistungsmessender Zähler
+    "MAXIMUMZAEHLER": "MAXIMUMZAEHLER",  # Maximumzähler
+    "TURBINENRADGASZAEHLER": "TURBINENRADGASZAEHLER",  # Turbinenradgaszähler
+    "ULTRASCHALLGASZAEHLER": "ULTRASCHALLGASZAEHLER",  # Ultraschallgaszähler
+    "WECHSELSTROMZAEHLER": "WECHSELSTROMZAEHLER",  # Wechselstromzähler
+}
+Zaehlertyp = Enum("Zaehlertyp", _zaehlertyp)
diff --git a/src/bo4e/enum/zeiteinheit.py b/src/bo4e/enum/zeiteinheit.py
new file mode 100644
index 000000000..afcbabd62
--- /dev/null
+++ b/src/bo4e/enum/zeiteinheit.py
@@ -0,0 +1,19 @@
+"""
+Auflistung möglicher Einheiten zur Verwendung in zeitbezogenen Angaben.
+"""
+
+from enum import Enum
+
+_zeiteinheit = {
+    "SEKUNDE": "SEKUNDE",  # Sekunde
+    "MINUTE": "MINUTE",  # Minute
+    "STUNDE": "STUNDE",  # Stunde
+    "VIERTEL_STUNDE": "VIERTEL_STUNDE",  # Viertelstunde
+    "TAG": "TAG",  # Tag
+    "WOCHE": "WOCHE",  # Woche
+    "MONAT": "MONAT",  # Monat
+    "QUARTAL": "QUARTAL",  # Quartal
+    "HALBJAHR": "HALBJAHR",  # Halbjahr
+    "JAHR": "JAHR",  # Jahr
+}
+Zeiteinheit = Enum("Zeiteinheit", _zeiteinheit)
diff --git a/src/bo4e/schemata/__init__.py b/src/bo4e/schemata/__init__.py
new file mode 100644
index 000000000..e69de29bb
diff --git a/src/bo4e/schemata/caseconverterschema.py b/src/bo4e/schemata/caseconverterschema.py
new file mode 100644
index 000000000..2fc93092b
--- /dev/null
+++ b/src/bo4e/schemata/caseconverterschema.py
@@ -0,0 +1,109 @@
+"""
+Functions and classes related to case conversions in schemata.
+"""
+import re
+from typing import Pattern
+
+from marshmallow import Schema, pre_load, post_dump
+
+
+def snake_to_lower_camel_case(string: str) -> str:
+    """
+    Converts the string s from snake_case to lowerCamelCase
+    :param string: foo_bar
+    :return: fooBar
+    """
+    # copied from https://marshmallow.readthedocs.io/en/stable/examples.html?highlight=case#inflection-camel-casing-keys
+    parts = iter(string.split("_"))
+    return next(parts) + "".join(i.title() for i in parts)
+
+
+_lower_camel_word_boundary_pattern: Pattern = re.compile("[^A-Z][A-Z]")
+
+
+def lower_camel_to_snake_case(string: str) -> str:
+    """
+    Converts the string s from lowerCamelCase to snake_case
+    :param string: fooBar
+    :return: foo_bar
+    """
+    result = string
+    for lower_upper in _lower_camel_word_boundary_pattern.findall(string):
+        result = result.replace(lower_upper, lower_upper[0] + "_" + lower_upper[1])
+    return result.lower()
+
+
+_upper_camel_word_boundary_pattern: Pattern = re.compile("[A-Z][^A-Z]")
+
+
+def upper_camel_to_snake_case(string: str) -> str:
+    """
+    Converts the string s from UpperCamel to snake_case
+    :param string: FooBar
+    :return: foo_bar
+    """
+    result = string
+    for upper_lower in _upper_camel_word_boundary_pattern.findall(string):
+        result = result.replace(upper_lower, upper_lower[0].lower() + "_" + upper_lower[1])
+    return result
+
+
+def to_snake_case(string: str) -> str:
+    """
+    Converts any string that is either lowerCamel or UpperCamel to snake_case
+    :param string: fooBar or FooBar
+    :return: foo_bar
+    """
+    result = lower_camel_to_snake_case(string)
+    result = upper_camel_to_snake_case(result)
+    return result
+
+
+class CaseConverterSchema(Schema):
+    """
+    Schema that uses snake case for its internal representation and lowerCamelCase for external representation unless
+    explicitly specified otherwise by using the data_key attribute.
+    """
+
+    def on_bind_field(self, field_name, field_obj):
+        """
+        if a data_key is not explicitly specified, then use the field name as lowerCamelCase
+        :param field_name:
+        :param field_obj:
+        :return:
+        """
+        field_obj.data_key = field_obj.data_key or to_snake_case(field_name)
+
+    @pre_load
+    # pylint:disable=unused-argument
+    def convert_keys_to_snake_case(self, data, many, **kwargs):
+        """
+        Converts all the keys of data to snake_case.
+        :param data:
+        :param many:
+        :param kwargs:
+        :return:
+        """
+        for external_key in list(data.keys()):
+            internal_key = to_snake_case(external_key)
+            if internal_key != external_key and internal_key in self.fields:
+                data[internal_key] = data.pop(external_key)
+        return data
+
+    @post_dump(pass_many=True)
+    def convert_keys_to_lower_camel(self, data, many, **kwargs):
+        """
+        Converts all the internal keys to lowerCamel keys
+        :param data:
+        :param many:
+        :param kwargs:
+        :return:
+        """
+        if many:
+            return [self.convert_keys_to_lower_camel(d, many=False, **kwargs) for d in data]
+        for internal_key in list(data.keys()):
+            if internal_key in self.fields:
+                external_key = snake_to_lower_camel_case(internal_key)
+                if external_key != internal_key:
+                    data[external_key] = data.pop(internal_key)
+        return data
diff --git a/tox.ini b/tox.ini
index 3cf803d6d..d4e14b461 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,52 +1,77 @@
 [tox]
-envlist = 
-    py{38}
+envlist =
+    tests
+    linting
+    coverage
     docs
-    flake8
 skip_missing_interpreters = True
 isolated_build = True
 
 [testenv]
+commands = python -m pip install --upgrade pip
+
+
+[testenv:tests]
 deps =
-    -r requirements/tests.txt
+    -rrequirements.txt
+    pytest
+    pytest-datafiles
+commands = python -m pytest --basetemp={envtmpdir} {posargs}
 
-commands = pytest --basetemp={envtmpdir} {posargs}
+
+[testenv:linting]
+deps =
+    -rrequirements.txt
+    pylint
+commands = pylint src/bo4e
+
+
+[testenv:coverage]
+usedevelop = True
+deps =
+    {[testenv:tests]deps}
+    coverage
+commands =
+    coverage run -m pytest --basetemp={envtmpdir} {posargs}
+    coverage html --omit .tox/*,tests/*
+    coverage report --fail-under 99 --omit .tox/*,tests/*
 
 
 [testenv:dev]
-description = development environment
-basepython = python3.8
 usedevelop = True
-commands = 
+deps =
+    {[testenv:tests]deps}
+    {[testenv:linting]deps}
+    {[testenv:coverage]deps}
+    black==21.5b2
+    # Replace by any tag/version: https://github.com/psf/black/tags
+    # and change the pre-commit-config.yaml accordingly
+    isort
+    pip-tools
+    pre-commit
+commands =
     python -m pip install --upgrade pip
-    pip install -q pip-tools
-    pip-compile requirements/base.in 
-    pip-compile requirements/dev.in 
-    pip-compile requirements/docs.in 
-    pip-compile requirements/tests.in
-    pip install -q -e . -r requirements/dev.txt
+    pip-compile requirements.in
+    pip install -r requirements.txt
+    pre-commit install
 
 
 [testenv:docs]
-deps = -r requirements/docs.txt
-commands = 
+deps =
+    -rrequirements.txt
+    Sphinx
+    sphinx_rtd_theme
+commands =
     sphinx-build -W -b html -d {envtmpdir}/doctrees docs {envtmpdir}/html
     sphinx-build -n -T -W -b doctest -d {envtmpdir}/doctrees docs docs/_build/html
     python -m doctest README.rst
 
 
-[testenv:flake8]
-basepython = python3.8
-skip_install = true
-deps = flake8
-commands = flake8
-
-
 [testenv:bo4e-from-pip]
 basepython = python3.8
 skip_install = true
 deps = -r requirements/tests.txt
-commands = 
+commands =
     pip install bo4e
     python -m pytest
 
@@ -59,51 +84,6 @@ deps =
     setuptools-scm
     wheel
     twine
-commands = 
+commands =
     python setup.py sdist bdist_wheel
     twine check dist/*
-
-
-[flake8]
-; can also be placed in .flake8, setup.cfg or in tox.ini
-exclude =
-    .git
-    .tox
-    .eggs
-extend-ignore = E203, W503, E501
-max-line-length = 120
-
-; ------------------------------------------
-
-; the following lines are not used now.
-; these are just some ideas for the future.
-
-; [testenv:black]
-; description = autoformat with black
-
-
-; [testenv:lint]
-; basepython = python3.8
-; skip_install = true
-; deps =
-;     pre-commit
-; passenv = HOMEPATH  # needed on Windows
-; commands =
-;     pre-commit run --all-files
-
-
-; [testenv:changelog]
-; basepython = python3.8
-; deps = towncrier
-; skip_install = true
-; commands = towncrier --draft
-
-
-; [testenv:coverage-report]
-; deps = coverage
-; skip_install = true
-; commands =
-;     coverage combine
-;     coverage report
-
-


Test patch:
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2475513f9..e393ae5cf 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -1,14 +1,13 @@
 name: Tests
 
-on: [push]
+on: [ push ]
 jobs:
   tests:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        python-version: [3.8]
-        os: [ubuntu-latest]
-        tox: [py38]
+        python-version: [ 3.8 ]
+        os: [ ubuntu-latest ]
     steps:
       - uses: actions/checkout@v2
       - name: Set up Python ${{ matrix.python-version }}
@@ -21,4 +20,4 @@ jobs:
           pip install tox
       - name: Test with tox
         run: |
-          tox -e ${{ matrix.tox }}
+          tox -e tests
diff --git a/requirements/tests.in b/requirements/tests.in
deleted file mode 100644
index e328502d1..000000000
--- a/requirements/tests.in
+++ /dev/null
@@ -1,4 +0,0 @@
--r base.in
-pytest
-pytest-datafiles
-flake8
\ No newline at end of file
diff --git a/requirements/tests.txt b/requirements/tests.txt
deleted file mode 100644
index bd070e742..000000000
--- a/requirements/tests.txt
+++ /dev/null
@@ -1,54 +0,0 @@
-#
-# This file is autogenerated by pip-compile
-# To update, run:
-#
-#    pip-compile requirements/tests.in
-#
-atomicwrites==1.4.0
-    # via pytest
-attrs==20.2.0
-    # via
-    #   -r requirements\base.in
-    #   pytest
-colorama==0.4.4
-    # via pytest
-flake8==3.8.4
-    # via -r requirements/tests.in
-iniconfig==1.1.1
-    # via pytest
-iso3166==1.0.1
-    # via -r requirements\base.in
-marshmallow-enum==1.5.1
-    # via -r requirements\base.in
-marshmallow==3.10.0
-    # via
-    #   -r requirements\base.in
-    #   marshmallow-enum
-mccabe==0.6.1
-    # via flake8
-packaging==20.4
-    # via pytest
-pluggy==0.13.1
-    # via pytest
-py==1.9.0
-    # via
-    #   pytest
-    #   pytest-datafiles
-pycodestyle==2.6.0
-    # via flake8
-pyflakes==2.2.0
-    # via flake8
-pyparsing==2.4.7
-    # via packaging
-pytest-datafiles==2.0
-    # via -r requirements/tests.in
-pytest==6.1.1
-    # via
-    #   -r requirements/tests.in
-    #   pytest-datafiles
-six==1.15.0
-    # via packaging
-stringcase==1.2.0
-    # via -r requirements\base.in
-toml==0.10.1
-    # via pytest
diff --git a/tests/test_adresse.py b/tests/test_adresse.py
index f0c35c43e..29eceddfb 100644
--- a/tests/test_adresse.py
+++ b/tests/test_adresse.py
@@ -1,6 +1,6 @@
 import json
-import pytest
 
+import pytest
 from bo4e.com.adresse import Adresse, AdresseSchema
 from bo4e.enum.landescode import Landescode
 
@@ -133,7 +133,6 @@ def test_missing_required_attribute(self, datafiles):
             address_test_data = json.load(json_file)
 
         with pytest.raises(TypeError) as excinfo:
-
             _ = Adresse(
                 ort=address_test_data["ort"],
                 strasse=address_test_data["strasse"],
@@ -192,9 +191,7 @@ def test_missing_required_attribute(self, datafiles):
         ],
     )
     def test_strasse_xor_postfach(self, address_test_data, expected):
-
         with pytest.raises(ValueError) as excinfo:
-
             _ = Adresse(
                 postleitzahl=address_test_data["postleitzahl"],
                 ort=address_test_data["ort"],
@@ -203,3 +200,17 @@ def test_strasse_xor_postfach(self, address_test_data, expected):
                 postfach=address_test_data["postfach"],
             )
         assert expected in str(excinfo.value)
+
+    def test_serialization_of_non_german_address(self):
+        """
+        Minimal working example
+        :return:
+        """
+        a = Adresse(
+            postleitzahl="6413", ort="Wildermieming", strasse="Gerhardhof", hausnummer="1", landescode=Landescode.AT
+        )
+        assert a.landescode == Landescode.AT
+        serialized_address = AdresseSchema().dumps(a)
+        assert '"AT"' in serialized_address
+        deserialized_address = AdresseSchema().loads(serialized_address)
+        assert deserialized_address.landescode == Landescode.AT
diff --git a/tests/test_ansprechpartner.py b/tests/test_ansprechpartner.py
new file mode 100644
index 000000000..3320b8d50
--- /dev/null
+++ b/tests/test_ansprechpartner.py
@@ -0,0 +1,121 @@
+from bo4e.bo.ansprechpartner import Ansprechpartner, AnsprechpartnerSchema
+from bo4e.bo.geschaeftspartner import Geschaeftspartner
+from bo4e.com.adresse import Adresse
+from bo4e.com.rufnummer import Rufnummer
+from bo4e.com.zustaendigkeit import Zustaendigkeit
+from bo4e.enum.anrede import Anrede
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
+from bo4e.enum.kontaktart import Kontaktart
+from bo4e.enum.rufnummernart import Rufnummernart
+from bo4e.enum.themengebiet import Themengebiet
+from bo4e.enum.titel import Titel
+
+
+class TestAnsprechpartner:
+    def test_de_serialisation_minimal_attributes(self):
+        """
+        Test de-/serialisation of Ansprechpartner only with required attributes
+        """
+        ansprechpartner = Ansprechpartner(
+            # required attributes
+            nachname="Müller-Schmidt",
+            geschaeftspartner=Geschaeftspartner(
+                anrede=Anrede.FRAU,
+                name1="von Sinnen",
+                name2="Helga",
+                name3=None,
+                gewerbekennzeichnung=True,
+                hrnummer="HRB 254466",
+                amtsgericht="Amtsgericht München",
+                kontaktweg=[Kontaktart.E_MAIL],
+                umsatzsteuer_id="DE267311963",
+                glaeubiger_id="DE98ZZZ09999999999",
+                e_mail_adresse="test@bo4e.de",
+                website="bo4e.de",
+                geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
+                partneradresse=Adresse(
+                    postleitzahl="12345",
+                    ort="Sankt Musterweiler",
+                    strasse="Mühlenweg",
+                    hausnummer="540a",
+                ),
+            ),
+        )
+        assert ansprechpartner.versionstruktur == "2", "versionstruktur was not automatically set"
+        assert ansprechpartner.bo_typ is BoTyp.ANSPRECHPARTNER, "boTyp was not automatically set"
+
+        schema = AnsprechpartnerSchema()
+        json_string = schema.dumps(ansprechpartner, ensure_ascii=False)
+        assert "Müller-Schmidt" in json_string
+        assert "Mühlenweg" in json_string
+
+        deserialized_ansprechpartner = schema.loads(json_data=json_string)
+
+        assert isinstance(deserialized_ansprechpartner, Ansprechpartner)
+        assert isinstance(deserialized_ansprechpartner.geschaeftspartner, Geschaeftspartner)
+        assert deserialized_ansprechpartner == ansprechpartner
+
+    def test_de_serialisation_maximal_attributes(self):
+        """
+        Test de-/serialisation of Ansprechpartner only with required attributes
+        """
+        ansprechpartner = Ansprechpartner(
+            # required attributes
+            nachname="Müller-Schmidt",
+            geschaeftspartner=Geschaeftspartner(
+                anrede=Anrede.FRAU,
+                name1="von Sinnen",
+                name2="Helga",
+                name3=None,
+                gewerbekennzeichnung=True,
+                hrnummer="HRB 254466",
+                amtsgericht="Amtsgericht München",
+                kontaktweg=[Kontaktart.E_MAIL],
+                umsatzsteuer_id="DE267311963",
+                glaeubiger_id="DE98ZZZ09999999999",
+                e_mail_adresse="test@bo4e.de",
+                website="bo4e.de",
+                geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
+                partneradresse=Adresse(
+                    postleitzahl="12345",
+                    ort="Sankt Musterweiler",
+                    strasse="Mühlenweg",
+                    hausnummer="540a",
+                ),
+            ),
+            # optional attributes
+            anrede=Anrede.EHELEUTE,
+            individuelle_anrede="Künstler",
+            titel=Titel.PROF_DR,
+            vorname="Hans",
+            e_mail_adresse="hans.müller@getrei.de",
+            kommentar="does this thing work?",
+            adresse=Adresse(
+                postleitzahl="12345",
+                ort="Sankt Musterweiler",
+                strasse="Mühlenweg",
+                hausnummer="540a",
+            ),
+            rufnummer=Rufnummer(nummerntyp=Rufnummernart.RUF_DURCHWAHL, rufnummer="112358132134"),
+            zustaendigkeit=Zustaendigkeit(
+                themengebiet=Themengebiet.MARKTKOMMUNIKATION, jobtitel="Schatzmeister", abteilung="unten rechts"
+            ),
+        )
+        assert ansprechpartner.versionstruktur == "2", "versionstruktur was not automatically set"
+        assert ansprechpartner.bo_typ is BoTyp.ANSPRECHPARTNER, "boTyp was not automatically set"
+
+        schema = AnsprechpartnerSchema()
+        json_string = schema.dumps(ansprechpartner, ensure_ascii=False)
+        assert "Müller-Schmidt" in json_string
+        assert "Mühlenweg" in json_string
+        assert "PROF_DR" in json_string
+
+        deserialized_ansprechpartner = schema.loads(json_data=json_string)
+
+        assert isinstance(deserialized_ansprechpartner, Ansprechpartner)
+        assert isinstance(deserialized_ansprechpartner.geschaeftspartner, Geschaeftspartner)
+        assert isinstance(deserialized_ansprechpartner.adresse, Adresse)
+        assert isinstance(deserialized_ansprechpartner.rufnummer, Rufnummer)
+        assert isinstance(deserialized_ansprechpartner.zustaendigkeit, Zustaendigkeit)
+        assert deserialized_ansprechpartner == ansprechpartner
diff --git a/tests/test_case_conversions.py b/tests/test_case_conversions.py
new file mode 100644
index 000000000..72e10c7ee
--- /dev/null
+++ b/tests/test_case_conversions.py
@@ -0,0 +1,165 @@
+from typing import Dict, Optional
+
+import attr
+import pytest
+from bo4e.schemata.caseconverterschema import to_snake_case, snake_to_lower_camel_case, CaseConverterSchema
+from marshmallow import fields, post_load
+
+
+@attr.s(auto_attribs=True)
+class MyClass:
+    some_string: str
+    an_integer: int
+    something: Optional[dict]
+
+
+class MySchema(CaseConverterSchema):
+    some_string = fields.Str(required=True, allow_none=False)
+    an_integer = fields.Integer(required=True, allow_none=False)
+    something = fields.Dict(required=False, allow_none=True)
+
+    @post_load
+    def to_my_class(self, data, **kwargs) -> MyClass:
+        return MyClass(**data)
+
+
+class MySchemaWithDataKeys(CaseConverterSchema):
+    """
+    The data_key attribute always allows to override the default behaviour.
+    """
+
+    some_string = fields.Str(required=True, allow_none=False, data_key="stringy key")
+    an_integer = fields.Integer(required=True, allow_none=False, data_key="intiBinty")
+    something = fields.Dict(required=False, allow_none=True, data_key="Some_Thing")
+
+    class Meta:
+        model = MyClass
+
+    @post_load
+    def to_my_class(self, data, **kwargs) -> MyClass:
+        return MyClass(**data)
+
+
+class TestCaseConversion:
+    """
+    Test class to test the snake/UpperCamel/lowerCamel conversions
+    """
+
+    @pytest.mark.parametrize(
+        "original, expected",
+        [
+            pytest.param("foo", "foo"),
+            pytest.param("foo_bar", "foo_bar"),
+            pytest.param("fooBar", "foo_bar"),
+            pytest.param("FooBar", "foo_bar"),
+            pytest.param("Foo", "foo"),
+            pytest.param("bo4e", "bo4e"),
+            pytest.param("Bo4e", "bo4e"),
+        ],
+    )
+    def test_to_snake(self, original: str, expected: str):
+        """
+        Tests the to_snake_case function
+        :param original: input string
+        :param expected: expected result
+        :return:
+        """
+        actual = to_snake_case(original)
+        assert actual == expected
+
+    @pytest.mark.parametrize(
+        "original, expected",
+        [
+            pytest.param("foo", "foo"),
+            pytest.param("foo_bar", "fooBar"),
+        ],
+    )
+    def test_to_lower_camel(self, original: str, expected: str):
+        """
+        Tests the to_lower_camel function
+        :param original: input string
+        :param expected: expected result
+        :return:
+        """
+        actual = snake_to_lower_camel_case(original)
+        assert actual == expected
+
+    @pytest.mark.parametrize(
+        "json_dict, expected",
+        [
+            pytest.param(
+                {"some_string": "asd", "an_integer": 17, "something": None},
+                MyClass(something=None, some_string="asd", an_integer=17),
+            ),
+            pytest.param(
+                {"someString": "asd", "anInteger": 17, "something": None},
+                MyClass(something=None, some_string="asd", an_integer=17),
+            ),
+            pytest.param(
+                {"SomeString": "asd", "AnInteger": 17, "Something": None},
+                MyClass(something=None, some_string="asd", an_integer=17),
+            ),
+        ],
+    )
+    def test_field_binding_on_load(self, json_dict: Dict[str, object], expected: MyClass):
+        """
+        Tests that the binding using the LowerCamelCaseSchema works
+        :return:
+        """
+        schema = MySchema()
+        actual: MyClass = schema.load(json_dict)
+        assert actual == expected
+
+    @pytest.mark.parametrize(
+        "json_dict, expected",
+        [
+            pytest.param(
+                {"stringy key": "asd", "intiBinty": 17, "Some_Thing": None},
+                MyClass(something=None, some_string="asd", an_integer=17),
+            )
+        ],
+    )
+    def test_field_binding_on_load_with_data_keys(self, json_dict: Dict[str, object], expected: MyClass):
+        """
+        Tests that the binding using the LowerCamelCaseSchema works and the data_key parameter is taken into account.
+        :return:
+        """
+        schema = MySchemaWithDataKeys()
+        actual: MyClass = schema.load(json_dict)
+        assert actual == expected
+
+    @pytest.mark.parametrize(
+        "instance, expected_dump",
+        [
+            pytest.param(
+                MyClass(something=None, some_string="asd", an_integer=17),
+                {"someString": "asd", "anInteger": 17, "something": None},
+            ),
+        ],
+    )
+    def test_key_conversion_on_dump(self, instance: MyClass, expected_dump: Dict[str, object]):
+        """
+        Tests that the dump using the LowerCamelCaseSchema works
+        :return:
+        """
+        schema = MySchema()
+        actual_dump: MyClass = schema.dump(instance)
+        assert actual_dump == expected_dump
+
+    @pytest.mark.parametrize(
+        "instance, expected_dump",
+        [
+            pytest.param(
+                MyClass(something=None, some_string="asd", an_integer=17),
+                {"stringy key": "asd", "intiBinty": 17, "Some_Thing": None},
+            ),
+        ],
+    )
+    def test_key_conversion_on_dump_with_data_keys(self, instance: MyClass, expected_dump: Dict[str, object]):
+        """
+        Tests that the dump using the LowerCamelCaseSchema works and the data_key argument is taken into account.
+        :return:
+        """
+        schema = MySchemaWithDataKeys()
+        actual_dump: MyClass = schema.dump(instance)
+        assert actual_dump == expected_dump
diff --git a/tests/test_data/test_data_adresse/test_data_adresse_missing_plz.json b/tests/test_data/test_data_adresse/test_data_adresse_missing_plz.json
index 1bc056f9d..00821a137 100644
--- a/tests/test_data/test_data_adresse/test_data_adresse_missing_plz.json
+++ b/tests/test_data/test_data_adresse/test_data_adresse_missing_plz.json
@@ -1,5 +1,5 @@
 {
-    "ort": "Grünwald",
-    "strasse": "Nördliche Müncher Straße",
-    "hausnummer": "27A"
+  "ort": "Grünwald",
+  "strasse": "Nördliche Müncher Straße",
+  "hausnummer": "27A"
 }
\ No newline at end of file
diff --git a/tests/test_data/test_data_adresse/test_data_adresse_only_required_fields.json b/tests/test_data/test_data_adresse/test_data_adresse_only_required_fields.json
index 2fbfcce88..d350a5952 100644
--- a/tests/test_data/test_data_adresse/test_data_adresse_only_required_fields.json
+++ b/tests/test_data/test_data_adresse/test_data_adresse_only_required_fields.json
@@ -1,6 +1,6 @@
 {
-    "postleitzahl": "82031",
-    "ort": "Grünwald",
-    "strasse": "Nördliche Münchner Straße",
-    "hausnummer": "27A"
+  "postleitzahl": "82031",
+  "ort": "Grünwald",
+  "strasse": "Nördliche Münchner Straße",
+  "hausnummer": "27A"
 }
\ No newline at end of file
diff --git a/tests/test_data/test_data_adresse/test_data_adresse_with_all_possible_fields.json b/tests/test_data/test_data_adresse/test_data_adresse_with_all_possible_fields.json
index 3b25c66c8..59d24d3ee 100644
--- a/tests/test_data/test_data_adresse/test_data_adresse_with_all_possible_fields.json
+++ b/tests/test_data/test_data_adresse/test_data_adresse_with_all_possible_fields.json
@@ -1,10 +1,10 @@
 {
-    "postleitzahl": "12345",
-    "ort": "ISS spacestation",
-    "strasse": "Milkyway",
-    "hausnummer": "42",
-    "postfach": "31415",
-    "adresszusatz": "to whom it may concern",
-    "co_ergaenzung": "you will find me",
-    "landescode": "FR"
+  "postleitzahl": "12345",
+  "ort": "ISS spacestation",
+  "strasse": "Milkyway",
+  "hausnummer": "42",
+  "postfach": "31415",
+  "adresszusatz": "to whom it may concern",
+  "co_ergaenzung": "you will find me",
+  "landescode": "FR"
 }
\ No newline at end of file
diff --git a/tests/test_geokoordinaten.py b/tests/test_geokoordinaten.py
index 7d2410260..e213ec69e 100644
--- a/tests/test_geokoordinaten.py
+++ b/tests/test_geokoordinaten.py
@@ -1,6 +1,6 @@
-import pytest
 from decimal import Decimal
 
+import pytest
 from bo4e.com.geokoordinaten import Geokoordinaten, GeokoordinatenSchema
 
 
@@ -26,7 +26,6 @@ def test_serialization(self):
 
     def test_wrong_datatype(self):
         with pytest.raises(TypeError) as excinfo:
-
             _ = Geokoordinaten(breitengrad="54,23", laengengrad=-23.2)
 
         assert "breitengrad" in str(excinfo.value)
diff --git a/tests/test_geschaeftsobjekt.py b/tests/test_geschaeftsobjekt.py
index a522fa80b..4731682cf 100644
--- a/tests/test_geschaeftsobjekt.py
+++ b/tests/test_geschaeftsobjekt.py
@@ -1,8 +1,7 @@
 import pytest
-
 from bo4e.bo.geschaeftsobjekt import Geschaeftsobjekt, GeschaeftsobjektSchema
-from bo4e.enum.botyp import BoTyp
 from bo4e.com.externereferenz import ExterneReferenz
+from bo4e.enum.botyp import BoTyp
 
 
 class TestGeschaeftsobjet:
@@ -39,11 +38,11 @@ def test_serialisation(self, bo_typ: BoTyp, versionstruktur: int, externe_refere
 
         assert str(versionstruktur) in go_json
 
-        go_deserialised = schema.loads(go_json)
+        go_deserialized = schema.loads(go_json)
 
-        assert go_deserialised.bo_typ is bo_typ
-        assert go_deserialised.versionstruktur == versionstruktur
-        assert go_deserialised.externe_referenzen == externe_referenzen
+        assert go_deserialized.bo_typ is bo_typ
+        assert go_deserialized.versionstruktur == versionstruktur
+        assert go_deserialized.externe_referenzen == externe_referenzen
 
     def test_initialization_with_minimal_attributs(self):
         go = Geschaeftsobjekt(bo_typ=BoTyp.ANSPRECHPARTNER)
diff --git a/tests/test_geschaeftspartner.py b/tests/test_geschaeftspartner.py
index 8a5bdaaa3..f045f16f0 100644
--- a/tests/test_geschaeftspartner.py
+++ b/tests/test_geschaeftspartner.py
@@ -1,18 +1,18 @@
-import pytest
 import json
 
+import pytest
 from bo4e.bo.geschaeftspartner import Geschaeftspartner, GeschaeftspartnerSchema
 from bo4e.com.adresse import Adresse
 from bo4e.enum.anrede import Anrede
 from bo4e.enum.botyp import BoTyp
 from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
 from bo4e.enum.kontaktart import Kontaktart
+from bo4e.enum.landescode import Landescode
 
 
 class TestGeschaeftspartner:
     @pytest.mark.datafiles("./tests/test_data/test_data_adresse/test_data_adresse_only_required_fields.json")
     def test_serializable(self, datafiles):
-
         with open(datafiles / "test_data_adresse_only_required_fields.json", encoding="utf-8") as json_file:
             address_test_data = json.load(json_file)
 
@@ -47,10 +47,10 @@ def test_serializable(self, datafiles):
 
         assert "Helga" in gp_json
 
-        gp_deserialised = schema.loads(gp_json)
+        gp_deserialized = schema.loads(gp_json)
 
-        assert gp_deserialised.bo_typ == gp.bo_typ
-        assert type(gp_deserialised.partneradresse) == Adresse
+        assert gp_deserialized.bo_typ == gp.bo_typ
+        assert type(gp_deserialized.partneradresse) == Adresse
 
     def test_optional_attribute_partneradresse(self):
         """
@@ -78,9 +78,9 @@ def test_optional_attribute_partneradresse(self):
 
         schema = GeschaeftspartnerSchema()
         gp_json = schema.dumps(gp, ensure_ascii=False)
-        gp_deserialised = schema.loads(gp_json)
+        gp_deserialized = schema.loads(gp_json)
 
-        assert gp_deserialised.partneradresse is None
+        assert gp_deserialized.partneradresse is None
 
     def test_list_validation_of_geschaeftspartnerrolle(self):
         """
@@ -92,7 +92,6 @@ def test_list_validation_of_geschaeftspartnerrolle(self):
         """
 
         with pytest.raises(TypeError) as excinfo:
-
             _ = Geschaeftspartner(
                 anrede=Anrede.FRAU,
                 name1="von Sinnen",
@@ -116,3 +115,27 @@ def test_list_validation_of_geschaeftspartnerrolle(self):
             )
 
         assert "must be typing.List" in str(excinfo.value)
+
+    def test_serialization_of_non_german_address(self):
+        """
+        Test that partneradresses with a Landescode!=DE (default) are (de)serialized correctly.
+        """
+        gp = Geschaeftspartner(
+            anrede=Anrede.FRAU,
+            name1="Kurz",
+            name2="Sebastian",
+            name3=None,
+            gewerbekennzeichnung=True,
+            hrnummer="HRB 254466",
+            amtsgericht="Amtsgericht Ibiza",
+            kontaktweg=[Kontaktart.E_MAIL],
+            umsatzsteuer_id="AT12345",
+            geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
+            partneradresse=Adresse(
+                postleitzahl="1014", ort="Wien 1", strasse="Ballhausplatz", hausnummer="2", landescode=Landescode.AT
+            ),
+        )
+        schema = GeschaeftspartnerSchema()
+        gp_json = schema.dumps(gp, ensure_ascii=False)
+        gp_deserialized = schema.loads(gp_json)
+        assert gp_deserialized.partneradresse.landescode == Landescode.AT
diff --git a/tests/test_marktlokation.py b/tests/test_marktlokation.py
index 5fa4c21b3..cc28c1e52 100644
--- a/tests/test_marktlokation.py
+++ b/tests/test_marktlokation.py
@@ -1,10 +1,9 @@
 import json
-import pytest
-
 from typing import Tuple
 
-from bo4e.bo.marktlokation import Marktlokation, MarktlokationSchema
+import pytest
 from bo4e.bo.geschaeftspartner import Geschaeftspartner
+from bo4e.bo.marktlokation import Marktlokation, MarktlokationSchema
 from bo4e.com.adresse import Adresse
 from bo4e.enum.anrede import Anrede
 from bo4e.enum.bilanzierungsmethode import Bilanzierungsmethode
diff --git a/tests/test_menge.py b/tests/test_menge.py
new file mode 100644
index 000000000..9232aa81b
--- /dev/null
+++ b/tests/test_menge.py
@@ -0,0 +1,37 @@
+from decimal import Decimal
+import pytest
+from bo4e.com.menge import Menge, MengeSchema
+from bo4e.enum.mengeneinheit import Mengeneinheit
+
+
+class TestMenge:
+    def test_menge(self):
+        """
+        Test de-/serialisation of Menge (only has required attributes).
+        """
+        menge = Menge(wert=Decimal(3.41), einheit=Mengeneinheit.MWH)
+
+        schema = MengeSchema()
+        json_string = schema.dumps(menge, ensure_ascii=False)
+
+        assert "3.41" in json_string
+        assert "MWH" in json_string
+
+        menge_deserialized = schema.loads(json_string)
+
+        assert isinstance(menge_deserialized.wert, Decimal)
+        assert menge_deserialized.wert == Decimal(3.41)
+        assert isinstance(menge_deserialized.einheit, Mengeneinheit)
+        assert menge_deserialized.einheit == Mengeneinheit.MWH
+
+    def test_wrong_datatype(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Menge(wert="3.14", einheit=Mengeneinheit.MWH)
+
+        assert "wert" in str(excinfo.value)
+
+    def test_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Menge(wert=Decimal(3.14))
+
+        assert "missing 1 required" in str(excinfo.value)
diff --git a/tests/test_messlokation.py b/tests/test_messlokation.py
new file mode 100644
index 000000000..15a35a32c
--- /dev/null
+++ b/tests/test_messlokation.py
@@ -0,0 +1,216 @@
+import json
+from datetime import datetime
+from decimal import Decimal
+
+import pytest
+
+from bo4e.bo.messlokation import Messlokation, MesslokationSchema
+from bo4e.bo.zaehler import Zaehler
+from bo4e.com.adresse import Adresse
+from bo4e.com.dienstleistung import Dienstleistung
+from bo4e.com.externereferenz import ExterneReferenz
+from bo4e.com.hardware import Hardware
+from bo4e.com.zaehlwerk import Zaehlwerk
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.dienstleistungstyp import Dienstleistungstyp
+from bo4e.enum.energierichtung import Energierichtung
+from bo4e.enum.geraetetyp import Geraetetyp
+from bo4e.enum.mengeneinheit import Mengeneinheit
+from bo4e.enum.netzebene import Netzebene
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.tarifart import Tarifart
+from bo4e.enum.zaehlerauspraegung import Zaehlerauspraegung
+from bo4e.enum.zaehlertyp import Zaehlertyp
+
+
+class TestMeLo:
+    def test_serialisation_only_required_attributes(self):
+        """
+        Test serialisation of Messlokation only with required attributes
+        """
+        melo = Messlokation(
+            messlokations_id="DE00056266802AO6G56M11SN51G21M24S",
+            sparte=Sparte.STROM,
+        )
+        assert melo.versionstruktur == "2", "versionstruktur was not automatically set"
+        assert melo.bo_typ is BoTyp.MESSLOKATION, "boTyp was not automatically set"
+
+        schema = MesslokationSchema()
+
+        json_string = schema.dumps(melo, ensure_ascii=False)
+        json_dict = json.loads(json_string)
+
+        assert "boTyp" in json_dict, "No camel case serialization"
+        assert "messlokationsId" in json_dict, "No camel case serialization"
+
+        deserialized_melo: Messlokation = schema.loads(json_string)
+
+        # check that `deserialized_malo.marktlokations_id` and `malo.marktlokations_id` have the same value
+        # but are **not** the same object.
+        assert deserialized_melo.messlokations_id == melo.messlokations_id
+        assert deserialized_melo.messlokations_id is not melo.messlokations_id
+        assert deserialized_melo.bo_typ is BoTyp.MESSLOKATION
+
+    def test_serialization_required_and_optional_attributes(self):
+        """
+        Test serialisation of Messlokation with required attributes and optional attributes
+        """
+
+        melo = Messlokation(
+            # required attributes
+            messlokations_id="DE00056266802AO6G56M11SN51G21M24S",
+            sparte=Sparte.STROM,
+            # optional attributes
+            netzebene_messung=Netzebene.MSP,
+            messgebietnr="664073",
+            geraete=[
+                Hardware(geraetetyp=Geraetetyp.INTELLIGENTES_MESSYSTEM, bezeichnung="intelligentes Messsystem"),
+                Hardware(geraetetyp=Geraetetyp.MODEM, bezeichnung="56k Modem"),
+            ],
+            messdienstleistung=[
+                Dienstleistung(
+                    dienstleistungstyp=Dienstleistungstyp.AUSLESUNG_TAEGLICH_FERNAUSLESUNG,
+                    bezeichnung="fernauslesung_taeglich",
+                ),
+                Dienstleistung(
+                    dienstleistungstyp=Dienstleistungstyp.ENTSPERRUNG,
+                    bezeichnung="entsperrung",
+                ),
+            ],
+            messlokationszaehler=[
+                Zaehler(
+                    zaehlernummer="000111222",
+                    sparte=Sparte.STROM,
+                    zaehlerauspraegung=Zaehlerauspraegung.EINRICHTUNGSZAEHLER,
+                    zaehlwerke=[
+                        Zaehlwerk(
+                            zaehlwerk_id="98765",
+                            einheit=Mengeneinheit.KW,
+                            richtung=Energierichtung.EINSP,
+                            bezeichnung="my zaehlwerk",
+                            obis_kennzahl="1-0:1.8.1",
+                            wandlerfaktor=Decimal(0.95),
+                        )
+                    ],
+                    zaehlertyp=Zaehlertyp.DREHSTROMZAEHLER,
+                    tarifart=Tarifart.ZWEITARIF,
+                    zaehlerkonstante=Decimal(0.9),
+                    eichung_bis=datetime(2022, 1, 1, 0, 0, 0),
+                    externe_referenzen=[ExterneReferenz(ex_ref_name="zaehler im anderen system", ex_ref_wert="7890")],
+                    letzte_eichung=datetime(2019, 6, 30, 0, 0, 0),
+                )
+            ],
+            grundzustaendiger_msb_codenr="9910125000002",
+            messadresse=Adresse(postleitzahl="04177", ort="Leipzig", hausnummer="1", strasse="Jahnalle"),
+        )
+        assert melo.versionstruktur == "2", "versionstruktur was not automatically set"
+        assert melo.bo_typ == BoTyp.MESSLOKATION, "boTyp was not automatically set"
+
+        schema = MesslokationSchema()
+
+        json_string = schema.dumps(melo, ensure_ascii=False)
+        json_dict = json.loads(json_string)
+
+        assert "boTyp" in json_dict, "No camel case serialization"
+        assert "messlokationsId" in json_dict, "No camel case serialization"
+
+        deserialized_melo: Messlokation = schema.loads(json_string)
+
+        assert deserialized_melo.messlokations_id == melo.messlokations_id
+        assert deserialized_melo.messlokations_id is not melo.messlokations_id
+        assert deserialized_melo.bo_typ is BoTyp.MESSLOKATION
+
+    def test_missing_required_fields(self):
+        """
+        Test that the required attributes are checked in the deserialization.
+        Therefore the required attribute `messlokations_id` is removed in the test data.
+        """
+        invalid_json_string = """
+            {
+                "boTyp": "MESSLOKATION",
+                "messlokationszaehler": null,
+                "katasterinformation": null,
+                "messgebietnr": "664073",
+                "messadresse":
+                    {
+                        "strasse": "Jahnalle",
+                        "hausnummer": "1",
+                        "postfach": null,
+                        "postleitzahl": "04177",
+                        "landescode": "DE",
+                        "coErgaenzung": null,
+                        "adresszusatz": null,
+                        "ort": "Leipzig"
+                    },
+                "sparte": "STROM",
+                "netzebeneMessung": "MSP",
+                "grundzustaendigerMsbCodenr": null,
+                "geraete": null,
+                "externeReferenzen": [],
+                "messdienstleistung": null,
+                "geoadresse": null,
+                "versionstruktur": "2",
+                "grundzustaendigerMsbimCodenr": null
+                }
+                """
+
+        schema = MesslokationSchema()
+
+        with pytest.raises(TypeError) as excinfo:
+            schema.loads(invalid_json_string)
+
+        assert "messlokations_id" in str(excinfo.value)
+
+    def test_address_validation(self):
+        with pytest.raises(ValueError) as excinfo:
+            _ = Messlokation(
+                messlokations_id="DE00056266802AO6G56M11SN51G21M24S",
+                sparte=Sparte.STROM,
+                netzebene_messung=Netzebene.MSP,
+                messadresse=Adresse(postleitzahl="04177", ort="Leipzig", hausnummer="1", strasse="Jahnalle"),
+                geoadresse="test",
+                katasterinformation="test",
+            )
+
+        assert str(excinfo.value) == "More than one address information is given."
+
+    def test_grundzustaendiger_x_codenr_validation(self):
+        with pytest.raises(ValueError) as excinfo:
+            _ = Messlokation(
+                messlokations_id="DE00056266802AO6G56M11SN51G21M24S",
+                sparte=Sparte.STROM,
+                netzebene_messung=Netzebene.MSP,
+                grundzustaendiger_msb_codenr="9904768000008",
+                grundzustaendiger_msbim_codenr="test",
+            )
+
+        assert str(excinfo.value) == "More than one codenr is given."
+
+    @pytest.mark.parametrize(
+        "melo_id, is_valid",
+        [
+            ("DE00056266802AO6G56M11SN51G21M24S", True),
+            ("FR00056266802AO6G56M11SN51G21M24S", True),
+            ("XX00056266802AO6G56M11SN51G21M24S", False),
+            ("0000056266802AO6G56M11SN51G21M24S", False),
+            ("asdasd", False),
+            ("   ", False),
+            ("  asdasdasd ", False),
+            ("keine melo id", False),
+            (None, False),
+            ("", False),
+        ],
+    )
+    def test_id_validation(self, melo_id: str, is_valid: bool):
+        def _instantiate_melo(melo_id: str):
+            _ = Messlokation(
+                messlokations_id=melo_id,
+                sparte=Sparte.STROM,
+                netzebene_messung=Netzebene.MSP,
+            )
+
+        if not is_valid:
+            with pytest.raises(ValueError):
+                _instantiate_melo(melo_id)
+        else:
+            _instantiate_melo(melo_id)
diff --git a/tests/test_messlokationszuordnung.py b/tests/test_messlokationszuordnung.py
index fd9105f80..5b00a40fb 100644
--- a/tests/test_messlokationszuordnung.py
+++ b/tests/test_messlokationszuordnung.py
@@ -1,9 +1,10 @@
+from datetime import datetime
+
 from bo4e.com.messlokationszuordnung import (
     Messlokationszuordnung,
     MesslokationszuordnungSchema,
 )
 from bo4e.enum.arithmetische_operation import ArithmetischeOperation
-from datetime import datetime
 
 
 class TestMesslokationszuordnung:
@@ -24,10 +25,10 @@ def test_serialisation_only_required_attributes(self):
         assert messlokations_id in json_string
         assert "ADDITION" in json_string
 
-        mlz_deserialised = schema.loads(json_string)
+        mlz_deserialized = schema.loads(json_string)
 
-        assert mlz_deserialised.messlokations_id == messlokations_id
-        assert mlz_deserialised.arithmetik == ArithmetischeOperation.ADDITION
+        assert mlz_deserialized.messlokations_id == messlokations_id
+        assert mlz_deserialized.arithmetik == ArithmetischeOperation.ADDITION
 
     def test_serialisation_required_and_optional_attributes(self):
         """
@@ -51,9 +52,9 @@ def test_serialisation_required_and_optional_attributes(self):
         assert mlz_dict["arithmetik"] == "ADDITION"
         assert mlz_dict["gueltigSeit"] == "2021-01-13T00:00:00"
 
-        mlz_deserialised = schema.load(mlz_dict)
+        mlz_deserialized = schema.load(mlz_dict)
 
-        assert mlz_deserialised.messlokations_id == messlokations_id
-        assert mlz_deserialised.arithmetik == ArithmetischeOperation.ADDITION
-        assert mlz_deserialised.gueltig_seit == datetime(year=2021, month=1, day=13)
-        assert mlz_deserialised.gueltig_bis == datetime(year=2021, month=5, day=4)
+        assert mlz_deserialized.messlokations_id == messlokations_id
+        assert mlz_deserialized.arithmetik == ArithmetischeOperation.ADDITION
+        assert mlz_deserialized.gueltig_seit == datetime(year=2021, month=1, day=13)
+        assert mlz_deserialized.gueltig_bis == datetime(year=2021, month=5, day=4)
diff --git a/tests/test_unterschrift.py b/tests/test_unterschrift.py
new file mode 100644
index 000000000..92fad6324
--- /dev/null
+++ b/tests/test_unterschrift.py
@@ -0,0 +1,49 @@
+from datetime import datetime, timezone
+import pytest
+from bo4e.com.unterschrift import Unterschrift, UnterschriftSchema
+
+
+class TestUnterschrift:
+    def test_unterschrift_only_required_attributes(self):
+        """
+        Test de-/serialisation of Unterschrift with minimal attributes.
+        """
+        unterschrift = Unterschrift(name="Foo")
+
+        schema = UnterschriftSchema()
+        json_string = schema.dumps(unterschrift, ensure_ascii=False)
+
+        assert "Foo" in json_string
+
+        unterschrift_deserialized = schema.loads(json_string)
+
+        assert isinstance(unterschrift_deserialized.name, str)
+        assert unterschrift_deserialized.name == "Foo"
+
+    def test_unterschrift_required_and_optional_attributes(self):
+        """
+        Test de-/serialisation of Unterschrift with maximal attributes.
+        """
+        unterschrift = Unterschrift(name="Foo", ort="Grünwald", datum=datetime(2019, 6, 7, tzinfo=timezone.utc))
+
+        schema = UnterschriftSchema()
+        json_string = schema.dumps(unterschrift, ensure_ascii=False)
+
+        assert "Foo" in json_string
+        assert "Grünwald" in json_string
+        assert "2019-06-07T00:00:00+00:00" in json_string
+
+        unterschrift_deserialized = schema.loads(json_string)
+
+        assert isinstance(unterschrift_deserialized.name, str)
+        assert unterschrift_deserialized.name == "Foo"
+        assert isinstance(unterschrift_deserialized.ort, str)
+        assert unterschrift_deserialized.ort == "Grünwald"
+        assert isinstance(unterschrift_deserialized.datum, datetime)
+        assert unterschrift_deserialized.datum == datetime(2019, 6, 7, tzinfo=timezone.utc)
+
+    def test_unterschrift_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Unterschrift()
+
+        assert "missing 1 required" in str(excinfo.value)
diff --git a/tests/test_vertrag.py b/tests/test_vertrag.py
new file mode 100644
index 000000000..77c1abca5
--- /dev/null
+++ b/tests/test_vertrag.py
@@ -0,0 +1,202 @@
+from datetime import datetime, timezone
+import pytest
+from bo4e.bo.geschaeftspartner import Geschaeftspartner
+from bo4e.bo.vertrag import Vertrag, VertragSchema
+from bo4e.com.unterschrift import Unterschrift
+from bo4e.com.vertragskonditionen import Vertragskonditionen
+from bo4e.com.vertragsteil import Vertragsteil
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.vertragsstatus import Vertragsstatus
+from bo4e.enum.anrede import Anrede
+from bo4e.enum.kontaktart import Kontaktart
+from bo4e.enum.geschaeftspartnerrolle import Geschaeftspartnerrolle
+from bo4e.com.adresse import Adresse
+from bo4e.enum.vertragsart import Vertragsart
+
+
+class TestVertrag:
+    _vertragsnummer = "1234567891011"
+    _vertragsart = Vertragsart.BILANZIERUNGSVERTRAG
+    _vertragsstatus = Vertragsstatus.AKTIV
+    _sparte = Sparte.STROM
+    _vertragsbeginn = datetime(2021, 4, 30, 13, 45, tzinfo=timezone.utc)
+    _vertragsende = datetime(2021, 6, 5, 16, 30, tzinfo=timezone.utc)
+    _vertragspartner1 = Geschaeftspartner(
+        anrede=Anrede.FRAU,
+        name1="von Sinnen",
+        name2="Helga",
+        name3=None,
+        gewerbekennzeichnung=True,
+        kontaktweg=[Kontaktart.E_MAIL],
+        umsatzsteuer_id="DE267311963",
+        glaeubiger_id="DE98ZZZ09999999999",
+        e_mail_adresse="test@bo4e.de",
+        website="bo4e.de",
+        geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
+        partneradresse=Adresse(
+            postleitzahl="24306",
+            ort="Plön",
+            strasse="Kirchstraße",
+            hausnummer="3",
+        ),
+    )
+    _vertragspartner2 = Geschaeftspartner(
+        name1="Eckart",
+        name2="Björn",
+        gewerbekennzeichnung=False,
+        geschaeftspartnerrolle=[Geschaeftspartnerrolle.DIENSTLEISTER],
+        partneradresse=Adresse(
+            postleitzahl="24211",
+            ort="Preetz",
+            strasse="Am Markt",
+            hausnummer="67",
+        ),
+    )
+    _vertragsteile = [
+        Vertragsteil(
+            vertragsteilbeginn=datetime(2021, 4, 30, tzinfo=timezone.utc),
+            vertragsteilende=datetime(2021, 6, 5, tzinfo=timezone.utc),
+        )
+    ]
+
+    def test_serialisation_only_required_attributes(self):
+        """
+        Test de-/serialisation of Vertrag with minimal attributes.
+        """
+        vertrag = Vertrag(
+            vertragsnummer=self._vertragsnummer,
+            vertragsart=self._vertragsart,
+            vertragsstatus=self._vertragsstatus,
+            sparte=self._sparte,
+            vertragsbeginn=self._vertragsbeginn,
+            vertragsende=self._vertragsende,
+            vertragspartner1=self._vertragspartner1,
+            vertragspartner2=self._vertragspartner2,
+            vertragsteile=self._vertragsteile,
+        )
+
+        schema = VertragSchema()
+        json_string = schema.dumps(vertrag, ensure_ascii=False)
+
+        assert vertrag.bo_typ is BoTyp.VERTRAG, "boTyp was not automatically set"
+        assert self._vertragsnummer in json_string
+        assert "BILANZIERUNGSVERTRAG" in json_string
+        assert "AKTIV" in json_string
+        assert "STROM" in json_string
+        assert "2021-04-30T13:45:00+00:00" in json_string
+        assert "2021-06-05T16:30:00+00:00" in json_string
+        assert "von Sinnen" in json_string
+        assert "Preetz" in json_string
+        assert "2021-06-05T00:00:00+00:00" in json_string
+
+        vertrag_deserialized = schema.loads(json_string)
+
+        assert vertrag_deserialized.vertragsnummer == self._vertragsnummer
+        assert vertrag_deserialized.vertragsart == self._vertragsart
+        assert vertrag_deserialized.vertragsstatus == self._vertragsstatus
+        assert vertrag_deserialized.sparte == self._sparte
+        assert vertrag_deserialized.vertragsbeginn == self._vertragsbeginn
+        assert vertrag_deserialized.vertragsende == self._vertragsende
+        assert vertrag_deserialized.vertragspartner1 == self._vertragspartner1
+        assert vertrag_deserialized.vertragspartner2 == self._vertragspartner2
+        assert vertrag_deserialized.vertragsteile == self._vertragsteile
+
+    def test_serialisation_required_and_optional_attributes(self):
+        """
+        Test de-/serialisation of Vertrag with maximal attributes.
+        """
+
+        vertrag = Vertrag(
+            vertragsnummer=self._vertragsnummer,
+            vertragsart=self._vertragsart,
+            vertragsstatus=self._vertragsstatus,
+            sparte=self._sparte,
+            vertragsbeginn=self._vertragsbeginn,
+            vertragsende=self._vertragsende,
+            vertragspartner1=self._vertragspartner1,
+            vertragspartner2=self._vertragspartner2,
+            vertragsteile=[
+                Vertragsteil(
+                    vertragsteilbeginn=datetime(2021, 4, 30, tzinfo=timezone.utc),
+                    vertragsteilende=datetime(2021, 6, 5, tzinfo=timezone.utc),
+                ),
+                Vertragsteil(
+                    vertragsteilbeginn=datetime(2001, 1, 23, tzinfo=timezone.utc),
+                    vertragsteilende=datetime(2002, 12, 3, tzinfo=timezone.utc),
+                ),
+            ],
+            beschreibung="Hello Vertrag",
+            vertragskonditionen=Vertragskonditionen(beschreibung="Beschreibung"),
+            unterzeichnervp1=[Unterschrift(name="Foo")],
+            unterzeichnervp2=[Unterschrift(name="Bar"), Unterschrift(name="Dr.No")],
+        )
+
+        schema = VertragSchema()
+        json_string = schema.dumps(vertrag, ensure_ascii=False)
+
+        assert vertrag.bo_typ is BoTyp.VERTRAG, "boTyp was not automatically set"
+        assert self._vertragsnummer in json_string
+        assert "BILANZIERUNGSVERTRAG" in json_string
+        assert "AKTIV" in json_string
+        assert "STROM" in json_string
+        assert "2021-04-30T13:45:00+00:00" in json_string
+        assert "2021-06-05T16:30:00+00:00" in json_string
+        assert "von Sinnen" in json_string
+        assert "Preetz" in json_string
+        assert "2021-06-05T00:00:00+00:00" in json_string
+        assert "2002-12-03T00:00:00+00:00" in json_string
+        assert "Hello Vertrag" in json_string
+        assert "Beschreibung" in json_string
+        assert "Foo" in json_string
+        assert "Bar" in json_string
+        assert "Dr.No" in json_string
+
+        vertrag_deserialized = schema.loads(json_string)
+
+        assert vertrag_deserialized.vertragsnummer == self._vertragsnummer
+        assert vertrag_deserialized.vertragsart == self._vertragsart
+        assert vertrag_deserialized.vertragsstatus == self._vertragsstatus
+        assert vertrag_deserialized.sparte == self._sparte
+        assert vertrag_deserialized.vertragsbeginn == self._vertragsbeginn
+        assert vertrag_deserialized.vertragsende == self._vertragsende
+        assert vertrag_deserialized.vertragspartner1 == self._vertragspartner1
+        assert vertrag_deserialized.vertragspartner2 == self._vertragspartner2
+        assert vertrag_deserialized.vertragsteile == [
+            Vertragsteil(
+                vertragsteilbeginn=datetime(2021, 4, 30, tzinfo=timezone.utc),
+                vertragsteilende=datetime(2021, 6, 5, tzinfo=timezone.utc),
+            ),
+            Vertragsteil(
+                vertragsteilbeginn=datetime(2001, 1, 23, tzinfo=timezone.utc),
+                vertragsteilende=datetime(2002, 12, 3, tzinfo=timezone.utc),
+            ),
+        ]
+        assert vertrag_deserialized.beschreibung == "Hello Vertrag"
+        assert vertrag_deserialized.vertragskonditionen == Vertragskonditionen(beschreibung="Beschreibung")
+        assert vertrag_deserialized.unterzeichnervp1 == [Unterschrift(name="Foo")]
+        assert vertrag_deserialized.unterzeichnervp2 == [Unterschrift(name="Bar"), Unterschrift(name="Dr.No")]
+
+    def test_missing_required_attributes(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Vertrag()
+
+        assert "missing 9 required" in str(excinfo.value)
+
+    def test_serialization_fails_for_empty_vertragsteile(self):
+        """
+        Test serialisation of Zaehler fails if there are no vertragsteile.
+        """
+        with pytest.raises(ValueError) as value_error:
+            _ = Vertrag(
+                vertragsnummer=self._vertragsnummer,
+                vertragsart=self._vertragsart,
+                vertragsstatus=self._vertragsstatus,
+                sparte=self._sparte,
+                vertragsbeginn=self._vertragsbeginn,
+                vertragsende=self._vertragsende,
+                vertragspartner1=self._vertragspartner1,
+                vertragspartner2=self._vertragspartner2,
+                vertragsteile=[],
+            )
+        assert value_error.value.args[0] == "The Vertrag must have at least 1 Vertragsteil"
diff --git a/tests/test_vertragskonditionen.py b/tests/test_vertragskonditionen.py
new file mode 100644
index 000000000..5403a3441
--- /dev/null
+++ b/tests/test_vertragskonditionen.py
@@ -0,0 +1,56 @@
+from datetime import datetime, timezone
+from decimal import Decimal
+from bo4e.com.vertragskonditionen import Vertragskonditionen, VertragskonditionenSchema
+from bo4e.com.zeitraum import Zeitraum
+from bo4e.enum.zeiteinheit import Zeiteinheit
+
+
+class TestVertragskonditionen:
+    def test_vertragskonditionen_with_optional_attributes(self):
+        """
+        Test de-/serialisation of Vertragskonditionen (only has optional attributes).
+        """
+        vertragskonditionen = Vertragskonditionen(
+            beschreibung="Foobar",
+            anzahl_abschlaege=Decimal(3),
+            vertragslaufzeit=Zeitraum(
+                startdatum=datetime(2012, 9, 21, tzinfo=timezone.utc),
+                enddatum=datetime(2013, 10, 11, tzinfo=timezone.utc),
+            ),
+            kuendigungsfrist=Zeitraum(einheit=Zeiteinheit.WOCHE, dauer=Decimal(3)),
+            vertragsverlaengerung=Zeitraum(einheit=Zeiteinheit.TAG, dauer=Decimal(14)),
+            abschlagszyklus=Zeitraum(einheit=Zeiteinheit.TAG, dauer=Decimal(5)),
+        )
+
+        schema = VertragskonditionenSchema()
+        json_string = schema.dumps(vertragskonditionen, ensure_ascii=False)
+
+        assert "Foobar" in json_string
+        assert "3" in json_string
+        assert "2013-10-11T00:00:00+00:00" in json_string
+        assert "WOCHE" in json_string
+        assert "TAG" in json_string
+        assert "14" in json_string
+
+        vertragskonditionen_deserialized = schema.loads(json_string)
+
+        assert isinstance(vertragskonditionen_deserialized.beschreibung, str)
+        assert vertragskonditionen_deserialized.beschreibung == "Foobar"
+        assert isinstance(vertragskonditionen_deserialized.anzahl_abschlaege, Decimal)
+        assert vertragskonditionen_deserialized.anzahl_abschlaege == Decimal(3)
+        assert isinstance(vertragskonditionen_deserialized.vertragslaufzeit, Zeitraum)
+        assert vertragskonditionen_deserialized.vertragslaufzeit == Zeitraum(
+            startdatum=datetime(2012, 9, 21, tzinfo=timezone.utc), enddatum=datetime(2013, 10, 11, tzinfo=timezone.utc)
+        )
+        assert isinstance(vertragskonditionen_deserialized.vertragsverlaengerung, Zeitraum)
+        assert vertragskonditionen_deserialized.vertragsverlaengerung == Zeitraum(
+            einheit=Zeiteinheit.TAG, dauer=Decimal(14)
+        )
+
+    def test_vertragskonditionen_empty(self):
+        """
+        Test empty Vertragskonditionen (only has optional attributes).
+        """
+        empty_vertragskonditionen = Vertragskonditionen()
+
+        assert isinstance(empty_vertragskonditionen, Vertragskonditionen)
diff --git a/tests/test_vertragsteil.py b/tests/test_vertragsteil.py
new file mode 100644
index 000000000..c367839cf
--- /dev/null
+++ b/tests/test_vertragsteil.py
@@ -0,0 +1,69 @@
+from datetime import datetime, timezone
+from decimal import Decimal
+import pytest
+from bo4e.com.menge import Menge
+from bo4e.com.vertragsteil import Vertragsteil, VertragsteilSchema
+from bo4e.enum.mengeneinheit import Mengeneinheit
+
+
+class TestVertragsteil:
+    def test_vertragsteil_only_required_attributes(self):
+        """
+        Test de-/serialisation of Vertragsteil with minimal attributes.
+        """
+        vertragsteil = Vertragsteil(
+            vertragsteilbeginn=datetime(2001, 3, 15, tzinfo=timezone.utc),
+            vertragsteilende=datetime(2007, 11, 27, tzinfo=timezone.utc),
+        )
+
+        schema = VertragsteilSchema()
+        json_string = schema.dumps(vertragsteil, ensure_ascii=False)
+
+        assert "2001-03-15T00:00:00+00:00" in json_string
+        assert "2007-11-27T00:00:00+00:00" in json_string
+
+        vertragsteil_deserialized = schema.loads(json_string)
+
+        assert isinstance(vertragsteil_deserialized.vertragsteilbeginn, datetime)
+        assert vertragsteil_deserialized.vertragsteilbeginn == datetime(2001, 3, 15, tzinfo=timezone.utc)
+        assert isinstance(vertragsteil_deserialized.vertragsteilende, datetime)
+        assert vertragsteil_deserialized.vertragsteilende == datetime(2007, 11, 27, tzinfo=timezone.utc)
+
+    def test_vertragsteil_required_and_optional_attributes(self):
+        """
+        Test de-/serialisation of Vertragsteil with maximal attributes.
+        """
+        vertragsteil = Vertragsteil(
+            vertragsteilbeginn=datetime(2001, 3, 15, tzinfo=timezone.utc),
+            vertragsteilende=datetime(2007, 11, 27, tzinfo=timezone.utc),
+            lokation="Bar",
+            vertraglich_fixierte_menge=Menge(wert=Decimal(3.1), einheit=Mengeneinheit.KWH),
+            minimale_abnahmemenge=Menge(wert=Decimal(2000), einheit=Mengeneinheit.KWH),
+            maximale_abnahmemenge=Menge(wert=Decimal(0.111111), einheit=Mengeneinheit.KWH),
+        )
+
+        schema = VertragsteilSchema()
+        json_string = schema.dumps(vertragsteil, ensure_ascii=False)
+
+        assert "2001-03-15T00:00:00+00:00" in json_string
+        assert "2007-11-27T00:00:00+00:00" in json_string
+        assert "Bar" in json_string
+        assert "KWH" in json_string
+        assert "0.111111" in json_string
+
+        vertragsteil_deserialized = schema.loads(json_string)
+
+        assert isinstance(vertragsteil_deserialized.vertragsteilbeginn, datetime)
+        assert vertragsteil_deserialized.vertragsteilbeginn == datetime(2001, 3, 15, tzinfo=timezone.utc)
+        assert isinstance(vertragsteil_deserialized.vertragsteilende, datetime)
+        assert vertragsteil_deserialized.vertragsteilende == datetime(2007, 11, 27, tzinfo=timezone.utc)
+        assert isinstance(vertragsteil_deserialized.lokation, str)
+        assert vertragsteil_deserialized.lokation == "Bar"
+        assert isinstance(vertragsteil_deserialized.minimale_abnahmemenge, Menge)
+        assert vertragsteil_deserialized.minimale_abnahmemenge == Menge(wert=Decimal(2000), einheit=Mengeneinheit.KWH)
+
+    def test_vertragsteil_missing_required_attribute(self):
+        with pytest.raises(TypeError) as excinfo:
+            _ = Vertragsteil(vertragsteilende=datetime(2007, 11, 27, tzinfo=timezone.utc))
+
+        assert "missing 1 required" in str(excinfo.value)
diff --git a/tests/test_zaehler.py b/tests/test_zaehler.py
new file mode 100644
index 000000000..be6509253
--- /dev/null
+++ b/tests/test_zaehler.py
@@ -0,0 +1,91 @@
+from datetime import datetime
+from decimal import Decimal
+
+import pytest
+from bo4e.bo.zaehler import Zaehler, ZaehlerSchema
+from bo4e.com.externereferenz import ExterneReferenz
+from bo4e.com.zaehlwerk import Zaehlwerk
+from bo4e.enum.botyp import BoTyp
+from bo4e.enum.energierichtung import Energierichtung
+from bo4e.enum.mengeneinheit import Mengeneinheit
+from bo4e.enum.sparte import Sparte
+from bo4e.enum.tarifart import Tarifart
+from bo4e.enum.zaehlerauspraegung import Zaehlerauspraegung
+from bo4e.enum.zaehlertyp import Zaehlertyp
+
+
+class TestZaehler:
+    def test_de_serialisation(self):
+        """
+        Test de-/serialisation of Zaehler only with required attributes
+        """
+        zaehler = Zaehler(
+            zaehlernummer="000111222",
+            sparte=Sparte.STROM,
+            zaehlerauspraegung=Zaehlerauspraegung.EINRICHTUNGSZAEHLER,
+            zaehlwerke=[
+                Zaehlwerk(
+                    zaehlwerk_id="98765",
+                    einheit=Mengeneinheit.KW,
+                    richtung=Energierichtung.EINSP,
+                    bezeichnung="my zaehlwerk",
+                    obis_kennzahl="1-0:1.8.1",
+                    wandlerfaktor=Decimal(0.95),
+                )
+            ],
+            zaehlertyp=Zaehlertyp.DREHSTROMZAEHLER,
+            tarifart=Tarifart.ZWEITARIF,
+            zaehlerkonstante=Decimal(0.9),
+            eichung_bis=datetime(2022, 1, 1, 0, 0, 0),
+            externe_referenzen=[ExterneReferenz(ex_ref_name="zaehler im anderen system", ex_ref_wert="7890")],
+            letzte_eichung=datetime(2019, 6, 30, 0, 0, 0),
+        )
+        assert zaehler.versionstruktur == "2", "versionstruktur was not automatically set"
+        assert zaehler.bo_typ is BoTyp.ZAEHLER, "boTyp was not automatically set"
+        assert zaehler.zaehlwerke[0].richtung == Energierichtung.EINSP
+        assert zaehler.zaehlwerke[0].einheit == Mengeneinheit.KW
+        schema = ZaehlerSchema()
+        json_string = schema.dumps(zaehler, ensure_ascii=False)
+        assert "richtung" in json_string, "Zaehlwerk->richtung was not serialized"
+        assert "einheit" in json_string, "Zaehlwerk->einheit was not serialized"
+        deserialized_zaehler = schema.loads(json_data=json_string)
+        assert deserialized_zaehler == zaehler
+
+    def test_serialization_fails_for_invalid_obis(self):
+        """
+        Test serialisation of Zaehler fails if OBIS is wrong.
+        """
+        with pytest.raises(ValueError) as value_error:
+            _ = Zaehler(
+                zaehlernummer="000111222",
+                sparte=Sparte.STROM,
+                zaehlerauspraegung=Zaehlerauspraegung.EINRICHTUNGSZAEHLER,
+                zaehlwerke=[
+                    Zaehlwerk(
+                        zaehlwerk_id="98765",
+                        einheit=Mengeneinheit.KW,
+                        richtung=Energierichtung.EINSP,
+                        bezeichnung="my zaehlwerk",
+                        obis_kennzahl="foo",  # <-- this is not a valid obis. it triggered the value error
+                        wandlerfaktor=Decimal(0.95),
+                    )
+                ],
+                zaehlertyp=Zaehlertyp.DREHSTROMZAEHLER,
+                tarifart=Tarifart.ZWEITARIF,
+            )
+        assert value_error.value.args[0].startswith("'obis_kennzahl' must match regex")
+
+    def test_serialization_fails_for_empty_zaehlwerke(self):
+        """
+        Test serialisation of Zaehler fails if there are no zaehlwerke.
+        """
+        with pytest.raises(ValueError) as value_error:
+            _ = Zaehler(
+                zaehlernummer="000111222",
+                sparte=Sparte.STROM,
+                zaehlerauspraegung=Zaehlerauspraegung.EINRICHTUNGSZAEHLER,
+                zaehlwerke=[],
+                zaehlertyp=Zaehlertyp.DREHSTROMZAEHLER,
+                tarifart=Tarifart.ZWEITARIF,
+            )
+        assert value_error.value.args[0] == "The Zaehler must have at least 1 Zaehlwerk"
diff --git a/tests/test_zeitraum.py b/tests/test_zeitraum.py
new file mode 100644
index 000000000..e3356a7bf
--- /dev/null
+++ b/tests/test_zeitraum.py
@@ -0,0 +1,103 @@
+from datetime import datetime, timezone
+from decimal import Decimal
+
+import pytest
+
+from bo4e.com.zeitraum import Zeitraum, ZeitraumSchema
+from bo4e.enum.zeiteinheit import Zeiteinheit
+
+
+class TestZeitraum:
+    def test_zeitraum_dauer(self):
+        """
+        Test de-/serialisation of Zeitraum (only has optional attributes) with option dauer and einheit.
+        """
+        zeitraum = Zeitraum(einheit=Zeiteinheit.TAG, dauer=Decimal(21))
+
+        schema = ZeitraumSchema()
+        json_string = schema.dumps(zeitraum, ensure_ascii=False)
+
+        assert "21" in json_string
+        assert "TAG" in json_string
+
+        zeitraum_deserialized = schema.loads(json_string)
+
+        assert isinstance(zeitraum_deserialized.einheit, Zeiteinheit)
+        assert zeitraum_deserialized.einheit == Zeiteinheit.TAG
+        assert isinstance(zeitraum_deserialized.dauer, Decimal)
+        assert zeitraum_deserialized.dauer == Decimal(21)
+
+    def test_zeitraum_daten(self):
+        """
+        Test de-/serialisation of Zeitraum (only has optional attributes) with option startdatum and enddatum.
+        """
+        zeitraum = Zeitraum(
+            startdatum=datetime(2013, 5, 1, tzinfo=timezone.utc), enddatum=datetime(2022, 1, 28, tzinfo=timezone.utc)
+        )
+
+        schema = ZeitraumSchema()
+        json_string = schema.dumps(zeitraum, ensure_ascii=False)
+
+        assert "2013-05-01T00:00:00+00:00" in json_string
+        assert "2022-01-28T00:00:00+00:00" in json_string
+
+        zeitraum_deserialized = schema.loads(json_string)
+
+        assert isinstance(zeitraum_deserialized.startdatum, datetime)
+        assert zeitraum_deserialized.startdatum == datetime(2013, 5, 1, tzinfo=timezone.utc)
+        assert isinstance(zeitraum_deserialized.enddatum, datetime)
+        assert zeitraum_deserialized.enddatum == datetime(2022, 1, 28, tzinfo=timezone.utc)
+
+    def test_zeitraum_zeitpunkte(self):
+        """
+        Test de-/serialisation of Zeitraum (only has optional attributes) with option startzeitpunkt and endzeitpunkt.
+        """
+        zeitraum = Zeitraum(
+            startzeitpunkt=datetime(2011, 2, 5, 16, 43, tzinfo=timezone.utc),
+            endzeitpunkt=datetime(2021, 7, 30, tzinfo=timezone.utc),
+        )
+
+        schema = ZeitraumSchema()
+        json_string = schema.dumps(zeitraum, ensure_ascii=False)
+
+        assert "2011-02-05T16:43:00+00:00" in json_string
+        assert "2021-07-30T00:00:00+00:00" in json_string
+
+        zeitraum_deserialized = schema.loads(json_string)
+
+        assert isinstance(zeitraum_deserialized.startzeitpunkt, datetime)
+        assert zeitraum_deserialized.startzeitpunkt == datetime(2011, 2, 5, 16, 43, tzinfo=timezone.utc)
+        assert isinstance(zeitraum_deserialized.endzeitpunkt, datetime)
+        assert zeitraum_deserialized.endzeitpunkt == datetime(2021, 7, 30, tzinfo=timezone.utc)
+
+    @pytest.mark.parametrize(
+        "arguments",
+        [
+            pytest.param({"startdatum": datetime(2013, 5, 1, tzinfo=timezone.utc)}),
+            pytest.param(
+                {
+                    "startdatum": datetime(2013, 5, 1, tzinfo=timezone.utc),
+                    "startzeitpunkt": datetime(2011, 2, 5, 16, 43, tzinfo=timezone.utc),
+                    "endzeitpunkt": datetime(2021, 7, 30, tzinfo=timezone.utc),
+                }
+            ),
+            pytest.param({}),
+            pytest.param(
+                {
+                    "startdatum": datetime(2013, 5, 1, tzinfo=timezone.utc),
+                    "endzeitpunkt": datetime(2021, 7, 30, tzinfo=timezone.utc),
+                }
+            ),
+        ],
+    )
+    def test_validator_time_range_possibilities(self, arguments):
+        with pytest.raises(ValueError) as excinfo:
+            _ = Zeitraum(**arguments)
+        assert """
+        Please choose from one of the three possibilities to specify the timerange:
+        - einheit and dauer
+        - startdatum and enddatum
+        - startzeitpunkt and endzeitpunkt
+        """ in str(
+            excinfo.value
+        )