Repository: COVESA/vss-tools (language: python)

Issue:
Simplify overlay handling
This is an idea for simplifying overlays. If a node is found in overlay without type (e.g. `branch`, `sensor`) or datatype (e.g. `uint8`) it will try to do a look-up if a node with the same name already exist, and then use that type/datatype value.

There are limitations, like if you add something to `Vehicle.Axle.Front.SomeSignal` in an overlay (i.e. only for a single instance) it will not find source signal `Vehicle.Axle.SomeSignal`.

Many of the changes are just adding license headers. As I got failure on some changed files I also updated some files that were not changed.

Next step if this is merged is to adapt documentation in VSS repo. Possibly saying something like: 

- _In most cases you do not need to specify `datatype` and `type` in overlay if you are modifying an existing signal._
- _Specifying `datatype` and `type` is needed if overlay addresses specific instances of a branch, like front left wheel._


Discussion / hints:
thx @erikbosch this is a great addition to implement issue https://github.com/COVESA/vss-tools/issues/193

I tested it on a clean install of vss-tools and is working as expected on non-struct signals. I haven't looked into the mentioned limitations on structs yet
@erikbosch I tested the structs and can confirm that I get the same warnings and errors. Now I was wondering what your planned next steps are on this topic?
I did not had time (or forgot) to mention the PRs in vss-tools yesterday, but as you have already looked at it I see no problems merging this one. I have no problems updating documentation, if you have time and interest in something for #193 feel free to try it out, or if you want to improve the limitations.
hi @erikbosch was there a specific reason why you didn't implement the limitations for structs? I would like to proceed with it to make https://github.com/COVESA/vss-tools/issues/193 work. An idea how you would overcome the limitations would be a valuable first step for me as I haven't worked much with the code that deals with structs.
For structs (i.e. type files) I did not really did any tests so there I do not really know how much effort, if any, that is required.

For instances we can take the example in https://github.com/COVESA/vehicle_signal_specification/pull/747

```YAML
# Type and Datatype not needed
Vehicle.Speed:
  unit: m/s

# Type and Datatype needed as overlay affects a particular instance
Vehicle.Occupant.Row1.DriverSide.HeadPosition.Yaw:
  type: sensor
  datatype: float
  unit: mm
```

Here the problem with the second one is that when we read the overlay we have not yet analyzed/expanded instances, so you can easily find `Vehicle.Occupant.HeadPosition.Yaw` in tree but you would need a more advanced search algorithm to find `Vehicle.Occupant.Row1.DriverSide.HeadPosition.Yaw` as `Row1` and `DriverSide` comes from instance information. Like finding `Vehicle.Occupant` and then check if `Row1` refers to a real sub-branch or rather to an instance. This is a bit related to the discussion in [#749](https://github.com/COVESA/vehicle_signal_specification/issues/749),
hi @erikbosch, sry about this, I accidentally called instances structs in this whole conversation and just realized it now.. I was referring to instances the entire time, as structs are not being used (yet) as far as I know.
I'm currently trying to come up with a search algorithm to find the definition of a signal that was instantiated to link its datatype and/or type to it when it is being overwritten from an overlay..
hi @erikbosch would it be possible to have a short 15-30min call about finding the type (and datatype) for instances? I've been trying to find a way to search for them but as this data is cleaned from the model before rendering a tree, I can't find a slick solution to retrieve the type and datatype. I would really appreciate it!!
@nwesem - we could try to align on a slot using the COVESA slack channel, if that works for you

Fix patch:
diff --git a/.github/workflows/buildcheck.yml b/.github/workflows/buildcheck.yml
index b4aac4d3..608daeb1 100644
--- a/.github/workflows/buildcheck.yml
+++ b/.github/workflows/buildcheck.yml
@@ -94,6 +94,6 @@ jobs:
         run: |
           pip install cyclonedds
           cd tests/ddsidl
-          vspec export ddsidl -u ../vspec/test_units.yaml -s test.vspec -o test.idl
+          vspec export ddsidl -u ../vspec/test_units.yaml -s test.vspec -o test.idl -q ../vspec/test_quantities.yaml
           idlc -l py test.idl
           grep -i A.String A/_test.py
diff --git a/.github/workflows/check-header.yml b/.github/workflows/check-header.yml
index c019dc0f..e85b3f77 100755
--- a/.github/workflows/check-header.yml
+++ b/.github/workflows/check-header.yml
@@ -21,7 +21,7 @@ jobs:
 
     - name: Get changed files
       run: |
-        echo "files=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep -v '^obsolete' | tr '\n' ',')" >> $GITHUB_ENV
+        echo "files=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep -v '^obsolete' | grep '^tests/.*\.py' |tr '\n' ',')" >> $GITHUB_ENV
 
     - uses: ./.github/actions/verify-headers
       with:
diff --git a/docs/tree.md b/docs/tree.md
new file mode 100644
index 00000000..0689770c
--- /dev/null
+++ b/docs/tree.md
@@ -0,0 +1,80 @@
+# Tree Exporter
+
+The `tree` exporter wraps [Anytree RenderTree](https://anytree.readthedocs.io/en/latest/api/anytree.render.html#anytree.render.RenderTree).
+Additionally it can include information you are interested in of the `VSSData` of every node.
+By default, the rendered tree will only print node names.
+The output file specified via `--output/-o` will be a simple text file containing the rendered tree.
+
+Example:
+```yaml
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Door:
+  type: branch
+  description: Door
+  instances:
+  - Row[1,2]
+  - Column[1,2]
+
+Vehicle.Door.Row1.Column1:
+  custom: foo
+```
+
+Result:
+```
+Vehicle
+└── Door
+    ├── Row1
+    │   ├── Column1
+    │   └── Column2
+    └── Row2
+        ├── Column1
+        └── Column2
+```
+
+If we also want to see the `description` of every node we can pass `--attr description`.
+
+Result:
+```
+Vehicle
+description='Vehicle'
+└── Door
+    description='Door'
+    ├── Row1
+    │   description='Door'
+    │   ├── Column1
+    │   │   description='Door'
+    │   └── Column2
+    │       description='Door'
+    └── Row2
+        description='Door'
+        ├── Column1
+        │   description='Door'
+        └── Column2
+            description='Door'
+```
+
+If we also want to show the custom attribute `custom` we can add another `-a custom`.
+
+Result:
+```
+Vehicle
+description='Vehicle'
+└── Door
+    description='Door'
+    ├── Row1
+    │   description='Door'
+    │   ├── Column1
+    │   │   description='Door'
+    │   │   custom='foo'
+    │   └── Column2
+    │       description='Door'
+    └── Row2
+        description='Door'
+        ├── Column1
+        │   description='Door'
+        └── Column2
+            description='Door'
+```
diff --git a/docs/vspec.md b/docs/vspec.md
index d8e99961..869162ba 100644
--- a/docs/vspec.md
+++ b/docs/vspec.md
@@ -22,6 +22,7 @@ vspec export --help
 │ jsonschema                        Export as a jsonschema.                                              │
 │ protobuf                          Export as protobuf.                                                  │
 │ yaml                              Export as YAML.                                                      │
+│ tree                              Export as Tree.                                                      │
 ╰────────────────────────────────────────────────────────────────────────────────────────────────────────╯
 ```
 
@@ -41,6 +42,13 @@ vspec export json --vspec spec/VehicleSignalSpecification.vspec --output vss.jso
            INFO     Serializing compact JSON...                                            vss2json.py:148
 
 ```
+## Exporter docs
+
+- [tree](./tree.md)
+- [ddsidl](./ddsidl.md)
+- [graphql](./graphql.md)
+- [id](./id.md)
+- [protobuf](./protobuf.md)
 
 ## Argument Explanations
 
diff --git a/poetry.lock b/poetry.lock
index a28034ce..3aef784b 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,5 +1,16 @@
 # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
 
+[[package]]
+name = "annotated-types"
+version = "0.7.0"
+description = "Reusable constraint types to use with typing.Annotated"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
+    {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
+]
+
 [[package]]
 name = "anytree"
 version = "2.12.1"
@@ -305,38 +316,38 @@ files = [
 
 [[package]]
 name = "mypy"
-version = "1.11.0"
+version = "1.11.1"
 description = "Optional static typing for Python"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "mypy-1.11.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a3824187c99b893f90c845bab405a585d1ced4ff55421fdf5c84cb7710995229"},
-    {file = "mypy-1.11.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:96f8dbc2c85046c81bcddc246232d500ad729cb720da4e20fce3b542cab91287"},
-    {file = "mypy-1.11.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a5d8d8dd8613a3e2be3eae829ee891b6b2de6302f24766ff06cb2875f5be9c6"},
-    {file = "mypy-1.11.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:72596a79bbfb195fd41405cffa18210af3811beb91ff946dbcb7368240eed6be"},
-    {file = "mypy-1.11.0-cp310-cp310-win_amd64.whl", hash = "sha256:35ce88b8ed3a759634cb4eb646d002c4cef0a38f20565ee82b5023558eb90c00"},
-    {file = "mypy-1.11.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:98790025861cb2c3db8c2f5ad10fc8c336ed2a55f4daf1b8b3f877826b6ff2eb"},
-    {file = "mypy-1.11.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:25bcfa75b9b5a5f8d67147a54ea97ed63a653995a82798221cca2a315c0238c1"},
-    {file = "mypy-1.11.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bea2a0e71c2a375c9fa0ede3d98324214d67b3cbbfcbd55ac8f750f85a414e3"},
-    {file = "mypy-1.11.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d2b3d36baac48e40e3064d2901f2fbd2a2d6880ec6ce6358825c85031d7c0d4d"},
-    {file = "mypy-1.11.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8e2e43977f0e09f149ea69fd0556623919f816764e26d74da0c8a7b48f3e18a"},
-    {file = "mypy-1.11.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:1d44c1e44a8be986b54b09f15f2c1a66368eb43861b4e82573026e04c48a9e20"},
-    {file = "mypy-1.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cea3d0fb69637944dd321f41bc896e11d0fb0b0aa531d887a6da70f6e7473aba"},
-    {file = "mypy-1.11.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a83ec98ae12d51c252be61521aa5731f5512231d0b738b4cb2498344f0b840cd"},
-    {file = "mypy-1.11.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:c7b73a856522417beb78e0fb6d33ef89474e7a622db2653bc1285af36e2e3e3d"},
-    {file = "mypy-1.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:f2268d9fcd9686b61ab64f077be7ffbc6fbcdfb4103e5dd0cc5eaab53a8886c2"},
-    {file = "mypy-1.11.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:940bfff7283c267ae6522ef926a7887305945f716a7704d3344d6d07f02df850"},
-    {file = "mypy-1.11.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:14f9294528b5f5cf96c721f231c9f5b2733164e02c1c018ed1a0eff8a18005ac"},
-    {file = "mypy-1.11.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d7b54c27783991399046837df5c7c9d325d921394757d09dbcbf96aee4649fe9"},
-    {file = "mypy-1.11.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:65f190a6349dec29c8d1a1cd4aa71284177aee5949e0502e6379b42873eddbe7"},
-    {file = "mypy-1.11.0-cp38-cp38-win_amd64.whl", hash = "sha256:dbe286303241fea8c2ea5466f6e0e6a046a135a7e7609167b07fd4e7baf151bf"},
-    {file = "mypy-1.11.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:104e9c1620c2675420abd1f6c44bab7dd33cc85aea751c985006e83dcd001095"},
-    {file = "mypy-1.11.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f006e955718ecd8d159cee9932b64fba8f86ee6f7728ca3ac66c3a54b0062abe"},
-    {file = "mypy-1.11.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:becc9111ca572b04e7e77131bc708480cc88a911adf3d0239f974c034b78085c"},
-    {file = "mypy-1.11.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6801319fe76c3f3a3833f2b5af7bd2c17bb93c00026a2a1b924e6762f5b19e13"},
-    {file = "mypy-1.11.0-cp39-cp39-win_amd64.whl", hash = "sha256:c1a184c64521dc549324ec6ef7cbaa6b351912be9cb5edb803c2808a0d7e85ac"},
-    {file = "mypy-1.11.0-py3-none-any.whl", hash = "sha256:56913ec8c7638b0091ef4da6fcc9136896914a9d60d54670a75880c3e5b99ace"},
-    {file = "mypy-1.11.0.tar.gz", hash = "sha256:93743608c7348772fdc717af4aeee1997293a1ad04bc0ea6efa15bf65385c538"},
+    {file = "mypy-1.11.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a32fc80b63de4b5b3e65f4be82b4cfa362a46702672aa6a0f443b4689af7008c"},
+    {file = "mypy-1.11.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c1952f5ea8a5a959b05ed5f16452fddadbaae48b5d39235ab4c3fc444d5fd411"},
+    {file = "mypy-1.11.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e1e30dc3bfa4e157e53c1d17a0dad20f89dc433393e7702b813c10e200843b03"},
+    {file = "mypy-1.11.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2c63350af88f43a66d3dfeeeb8d77af34a4f07d760b9eb3a8697f0386c7590b4"},
+    {file = "mypy-1.11.1-cp310-cp310-win_amd64.whl", hash = "sha256:a831671bad47186603872a3abc19634f3011d7f83b083762c942442d51c58d58"},
+    {file = "mypy-1.11.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7b6343d338390bb946d449677726edf60102a1c96079b4f002dedff375953fc5"},
+    {file = "mypy-1.11.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e4fe9f4e5e521b458d8feb52547f4bade7ef8c93238dfb5bbc790d9ff2d770ca"},
+    {file = "mypy-1.11.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:886c9dbecc87b9516eff294541bf7f3655722bf22bb898ee06985cd7269898de"},
+    {file = "mypy-1.11.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:fca4a60e1dd9fd0193ae0067eaeeb962f2d79e0d9f0f66223a0682f26ffcc809"},
+    {file = "mypy-1.11.1-cp311-cp311-win_amd64.whl", hash = "sha256:0bd53faf56de9643336aeea1c925012837432b5faf1701ccca7fde70166ccf72"},
+    {file = "mypy-1.11.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:f39918a50f74dc5969807dcfaecafa804fa7f90c9d60506835036cc1bc891dc8"},
+    {file = "mypy-1.11.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0bc71d1fb27a428139dd78621953effe0d208aed9857cb08d002280b0422003a"},
+    {file = "mypy-1.11.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b868d3bcff720dd7217c383474008ddabaf048fad8d78ed948bb4b624870a417"},
+    {file = "mypy-1.11.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a707ec1527ffcdd1c784d0924bf5cb15cd7f22683b919668a04d2b9c34549d2e"},
+    {file = "mypy-1.11.1-cp312-cp312-win_amd64.whl", hash = "sha256:64f4a90e3ea07f590c5bcf9029035cf0efeae5ba8be511a8caada1a4893f5525"},
+    {file = "mypy-1.11.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:749fd3213916f1751fff995fccf20c6195cae941dc968f3aaadf9bb4e430e5a2"},
+    {file = "mypy-1.11.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b639dce63a0b19085213ec5fdd8cffd1d81988f47a2dec7100e93564f3e8fb3b"},
+    {file = "mypy-1.11.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c956b49c5d865394d62941b109728c5c596a415e9c5b2be663dd26a1ff07bc0"},
+    {file = "mypy-1.11.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:45df906e8b6804ef4b666af29a87ad9f5921aad091c79cc38e12198e220beabd"},
+    {file = "mypy-1.11.1-cp38-cp38-win_amd64.whl", hash = "sha256:d44be7551689d9d47b7abc27c71257adfdb53f03880841a5db15ddb22dc63edb"},
+    {file = "mypy-1.11.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2684d3f693073ab89d76da8e3921883019ea8a3ec20fa5d8ecca6a2db4c54bbe"},
+    {file = "mypy-1.11.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:79c07eb282cb457473add5052b63925e5cc97dfab9812ee65a7c7ab5e3cb551c"},
+    {file = "mypy-1.11.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11965c2f571ded6239977b14deebd3f4c3abd9a92398712d6da3a772974fad69"},
+    {file = "mypy-1.11.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a2b43895a0f8154df6519706d9bca8280cda52d3d9d1514b2d9c3e26792a0b74"},
+    {file = "mypy-1.11.1-cp39-cp39-win_amd64.whl", hash = "sha256:1a81cf05975fd61aec5ae16501a091cfb9f605dc3e3c878c0da32f250b74760b"},
+    {file = "mypy-1.11.1-py3-none-any.whl", hash = "sha256:0624bdb940255d2dd24e829d99a13cfeb72e4e9031f9492148f410ed30bcab54"},
+    {file = "mypy-1.11.1.tar.gz", hash = "sha256:f404a0b069709f18bbdb702eb3dcfe51910602995de00bd39cea3050b5772d08"},
 ]
 
 [package.dependencies]
@@ -416,13 +427,13 @@ testing = ["pytest", "pytest-benchmark"]
 
 [[package]]
 name = "pre-commit"
-version = "3.7.1"
+version = "3.8.0"
 description = "A framework for managing and maintaining multi-language pre-commit hooks."
 optional = false
 python-versions = ">=3.9"
 files = [
-    {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"},
-    {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"},
+    {file = "pre_commit-3.8.0-py2.py3-none-any.whl", hash = "sha256:9a90a53bf82fdd8778d58085faf8d83df56e40dfe18f45b19446e26bf1b3a63f"},
+    {file = "pre_commit-3.8.0.tar.gz", hash = "sha256:8bb6494d4a20423842e198980c9ecf9f96607a07ea29549e180eef9ae80fe7af"},
 ]
 
 [package.dependencies]
@@ -443,6 +454,129 @@ files = [
     {file = "pycodestyle-2.11.1.tar.gz", hash = "sha256:41ba0e7afc9752dfb53ced5489e89f8186be00e599e712660695b7a75ff2663f"},
 ]
 
+[[package]]
+name = "pydantic"
+version = "2.8.2"
+description = "Data validation using Python type hints"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"},
+    {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"},
+]
+
+[package.dependencies]
+annotated-types = ">=0.4.0"
+pydantic-core = "2.20.1"
+typing-extensions = [
+    {version = ">=4.12.2", markers = "python_version >= \"3.13\""},
+    {version = ">=4.6.1", markers = "python_version < \"3.13\""},
+]
+
+[package.extras]
+email = ["email-validator (>=2.0.0)"]
+
+[[package]]
+name = "pydantic-core"
+version = "2.20.1"
+description = "Core functionality for Pydantic validation and serialization"
+optional = false
+python-versions = ">=3.8"
+files = [
+    {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"},
+    {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"},
+    {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"},
+    {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"},
+    {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"},
+    {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"},
+    {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"},
+    {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"},
+    {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"},
+    {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"},
+    {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"},
+    {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"},
+    {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"},
+    {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"},
+    {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"},
+    {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"},
+    {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"},
+    {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"},
+    {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"},
+    {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"},
+    {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"},
+    {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"},
+]
+
+[package.dependencies]
+typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0"
+
 [[package]]
 name = "pyflakes"
 version = "3.1.0"
@@ -676,13 +810,13 @@ files = [
 
 [[package]]
 name = "types-setuptools"
-version = "71.1.0.20240724"
+version = "71.1.0.20240726"
 description = "Typing stubs for setuptools"
 optional = false
 python-versions = ">=3.8"
 files = [
-    {file = "types-setuptools-71.1.0.20240724.tar.gz", hash = "sha256:ae61c528111a9450e149aedbc1ab0f7874f9bb0dc8f14c3cd200a1f82457050b"},
-    {file = "types_setuptools-71.1.0.20240724-py3-none-any.whl", hash = "sha256:90570f0578ed67bb25ada9b3d1432e6727178d2408a7051b27e4a712731df30a"},
+    {file = "types-setuptools-71.1.0.20240726.tar.gz", hash = "sha256:85ba28e9461bb1be86ebba4db0f1c2408f2b11115b1966334ea9dc464e29303e"},
+    {file = "types_setuptools-71.1.0.20240726-py3-none-any.whl", hash = "sha256:a7775376f36e0ff09bcad236bf265777590a66b11623e48c20bfc30f1444ea36"},
 ]
 
 [[package]]
@@ -734,4 +868,4 @@ test = ["big-O", "importlib-resources", "jaraco.functools", "jaraco.itertools",
 [metadata]
 lock-version = "2.0"
 python-versions = "^3.10"
-content-hash = "f9580af0cae6fef1f2d7e0406beb4fa36821c6fd1c6a67691e297b8b689d6d7c"
+content-hash = "78edfce80a4ad6b2eab7e97652083dd1762bd2203b4878ebf7828d261a2e77f5"
diff --git a/pyproject.toml b/pyproject.toml
index 779922d1..15e4fc80 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -18,6 +18,7 @@ rdflib = "^6.0.0"
 importlib-metadata = "^7.0"
 click = "^8.1.7"
 rich-click = "^1.8.3"
+pydantic = "^2.8.2"
 
 [tool.poetry.group.dev.dependencies]
 mypy = "*"
@@ -30,8 +31,11 @@ pre-commit = "*"
 
 
 [tool.poetry.scripts]
-vspec = "vss_tools.vspec.vspec:cli"
+vspec = "vss_tools.vspec.cli:cli"
 
 [build-system]
 requires = ["poetry-core"]
 build-backend = "poetry.core.masonry.api"
+
+[tool.mypy]
+disable_error_code = "import-untyped"
diff --git a/src/vss_tools/vspec/__init__.py b/src/vss_tools/vspec/__init__.py
index c4d667a5..3b2f4a7b 100755
--- a/src/vss_tools/vspec/__init__.py
+++ b/src/vss_tools/vspec/__init__.py
@@ -5,1006 +5,3 @@
 # https://www.mozilla.org/en-US/MPL/2.0/
 #
 # SPDX-License-Identifier: MPL-2.0
-
-#
-# VSpec file parser.
-#
-
-import yaml
-import os
-import uuid
-import sys
-import collections
-from copy import deepcopy
-from typing import List, Optional
-
-from anytree import (Resolver, LevelOrderIter, PreOrderIter, RenderTree)  # type: ignore[import]
-
-from vss_tools import log
-from vss_tools.vspec.model.vsstree import VSSNode, VSSType
-from vss_tools.vspec.model.exceptions import ImpossibleMergeException, IncompleteElementException
-from vss_tools.vspec.model.constants import VSSTreeType, VSSUnitCollection, VSSQuantityCollection
-
-nestable_types = set(["branch", "struct"])
-
-
-class VSpecError(Exception):
-    def __init__(self, *args, **kwargs):
-        self.file_name = args[0]
-        self.line_nr = args[1]
-        self.message = args[2]
-        Exception.__init__(self, *args, **kwargs)
-
-    def __str__(self):
-        return "{}: {}: {}".format(self.file_name, self.line_nr, self.message)
-
-
-# Try to open a file name that can reside
-# in any directory listed in incude_paths.
-# If successful, read context and return file
-#
-def search_and_read(file_name, include_paths):
-    # If absolute path, then ignore include paths
-    if file_name[0] == '/':
-        with open(file_name, "r") as fp:
-            text = fp.read()
-            fp.close()
-            return os.path.dirname(file_name), text
-
-    for directory in include_paths:
-        try:
-            path = "{}/{}".format(directory, file_name)
-            with open(path, "r") as fp:
-                text = fp.read()
-                fp.close()
-                return os.path.dirname(path), text
-        except IOError:
-            pass
-
-    # We failed, raise last exception we ran into.
-    raise VSpecError(file_name, 0, "File error")
-
-
-def convert_yaml_to_list(raw_yaml):
-    if isinstance(raw_yaml, list):
-        return raw_yaml
-
-    # Sort the dictionary according to line number.
-    # The reason is that when the YAML file is loaded
-    # the object order is not preserved in the created
-    # dictionary
-    raw_yaml = collections.OrderedDict(
-        sorted(raw_yaml.items(), key=lambda x: x[1]['$line$']))
-    lst = []
-    for elem in raw_yaml:
-        if isinstance(raw_yaml[elem], dict):
-            raw_yaml[elem]['$name$'] = elem
-            lst.append(raw_yaml[elem])
-
-    return lst
-
-
-def load_tree(
-        file_name,
-        include_paths,
-        tree_type: VSSTreeType,
-        break_on_name_style_violation=False,
-        expand_inst=True,
-        data_type_tree: Optional[VSSNode] = None):
-    if expand_inst and tree_type == VSSTreeType.DATA_TYPE_TREE:
-        log.error("Instance expansion is not supported for VSS type tree.")
-        sys.exit(-1)
-
-    flat_model = load_flat_model(file_name, "", include_paths, tree_type)
-    absolute_path_flat_model = create_absolute_paths(flat_model)
-    deep_model = create_nested_model(absolute_path_flat_model, file_name)
-    cleanup_deep_model(deep_model)
-    dict_tree = deep_model["children"]
-    tree = render_tree(
-        dict_tree,
-        tree_type,
-        break_on_name_style_violation=break_on_name_style_violation)
-    if expand_inst:
-        expand_tree_instances(tree)
-    return tree
-
-
-def check_type_usage(tree: VSSNode, tree_type: VSSTreeType, type_tree: Optional[VSSNode] = None):
-    """
-    Check usages of datatypes within the tree.
-    This methods shall be called after overlays (or additional type files) have been merged.
-    """
-    log.info("Check type usage")
-
-    if tree_type == VSSTreeType.DATA_TYPE_TREE:
-
-        check_data_type_references(tree)
-
-    elif tree_type == VSSTreeType.SIGNAL_TREE:
-        check_data_type_references_across_trees(tree, type_tree)
-
-
-def load_flat_model(file_name, prefix, include_paths, tree_type: VSSTreeType):
-    # Hooks into YAML parser to add line numbers
-    # and file name into each element
-    def yaml_compose_node(parent, index):
-        # the line number where the previous token has ended (plus empty lines)
-        line = loader.line
-        try:
-            node = yaml.composer.Composer.compose_node(loader, parent, index)
-        except yaml.scanner.ScannerError as e:
-            raise VSpecError(file_name, line + 1, e)
-        except yaml.parser.ParserError as e:
-            raise VSpecError(file_name, line + 1, e)
-
-        if node.value == '$include$':
-            node.value = f'$include${load_flat_model.include_index}'
-            load_flat_model.include_index = load_flat_model.include_index + 1
-
-        # Avoid having root-level line numbers as non-dictionary entries
-        if parent:
-            node.__line__ = line + 1
-            node.__file_name__ = file_name
-        else:
-            node.__line__ = None
-            node.__file_name = None
-        return node
-
-    def yaml_construct_mapping(node, deep=True):
-        mapping = yaml.constructor.Constructor.construct_mapping(
-            loader, node, deep=deep)
-
-        # Replace
-        # { 'Vehicle.Speed': { 'datatype': 'boolean', 'type': 'sensor' }}
-        # with
-        # { '$name$': 'Vehicle.Speed', 'datatype': 'boolean', 'type': 'sensor' }
-
-        for key, val in list(mapping.items()):
-            if key[0] == '$':
-                continue
-
-            if val is None:
-                mapping['$name$'] = key
-                del mapping[key]
-                break
-
-        # Add line number and file name to element.
-        if node.__line__ is not None:
-            mapping['$line$'] = node.__line__
-            mapping['$file_name$'] = node.__file_name__
-
-        return mapping
-
-    directory, text = search_and_read(file_name, include_paths)
-
-    # Do a trial pasing of the file to find out if it is list- or
-    # object-formatted.
-    loader = yaml.Loader(text)
-    loader.compose_node = yaml_compose_node  # type: ignore[assignment]
-
-    loader.construct_mapping = yaml_construct_mapping  # type: ignore[assignment]
-    test_yaml = loader.get_data()
-
-    # Depending on if this is a list or an object, expand
-    # the #include diretives differently
-    #
-    if isinstance(test_yaml, list):
-        text = yamilify_includes(text, True)
-    else:
-        text = yamilify_includes(text, False)
-
-    # Re-initialize loader with the new text hosting the
-    # yamilified includes.
-    loader = yaml.Loader(text)
-    loader.compose_node = yaml_compose_node  # type: ignore[assignment]
-
-    loader.construct_mapping = yaml_construct_mapping  # type: ignore[assignment]
-    raw_yaml = loader.get_data()
-
-    # Check for file with no objects.
-    if not raw_yaml:
-        return []
-
-    raw_yaml = convert_yaml_to_list(raw_yaml)
-
-    # Sanity check of loaded code
-    check_yaml_usage(raw_yaml, file_name)
-
-    # Recursively expand all include files.
-    if directory not in include_paths:
-        include_paths = [directory] + include_paths
-    expanded_includes = expand_includes(
-        raw_yaml, prefix, include_paths, tree_type)
-
-    flat_model = cleanup_flat_entries(expanded_includes, tree_type)
-
-    return flat_model
-
-
-def cleanup_flat_entries(flat_model, tree_type: VSSTreeType):
-    """
-    # 1. Check that allowed values are provided as arrays.
-    """
-
-    # Traverse the flat list of the parsed specification
-    for elem in flat_model:
-        # Type check moved later to allow for inherited types
-        # That is intended for overlay use-cases
-
-        if "allowed" in elem and not isinstance(elem["allowed"], list):
-            raise VSpecError(elem["$file_name$"], elem["$line$"],
-                             "Allowed values are not represented as array.")
-
-    return flat_model
-
-
-#
-# Delete parser-specific elements
-#
-# Parser metadata is cleaned in two steps. An initial step just after vspec files are parsed.
-# Then some data is removed but not all as it is used for error messages.
-# That needs to be removed in a second step, just before exporting.
-#
-def cleanup_deep_model(deep_model):
-
-    if "$line$" in deep_model:
-        del deep_model["$line$"]
-
-    if "$prefix$" in deep_model:
-        del deep_model["$prefix$"]
-
-    if "$name$" in deep_model:
-        del deep_model['$name$']
-
-    # children as of today exists only for branches and structs
-    if "children" in deep_model:
-        children = deep_model["children"]
-        for child in deep_model["children"]:
-            cleanup_deep_model(children[child])
-
-    return None
-
-#
-# Meta data on extended attributes needs to be cleaned as part of the second cleaning step.
-# as it is not included in first step.
-#
-
-
-def clean_metadata(node):
-
-    if isinstance(node, VSSNode):
-        clean_metadata(node.extended_attributes)
-        for child in node.children:
-            clean_metadata(child)
-    elif isinstance(node, dict):
-        for k in list(node.keys()):
-            clean_metadata(node[k])
-            if k in ["$file_name$", "$line$"]:
-                del node[k]
-    elif isinstance(node, list):
-        for elem in node:
-            clean_metadata(elem)
-
-
-def verify_mandatory_attributes(node, abort_on_unknown_attribute: bool):
-    """
-    Verify that mandatory attributes are present.
-    Need to be checked first after overlays (if any) have been applied, as attributes are not
-    mandatory in individual files but only in the final tree
-    """
-    if isinstance(node, VSSNode):
-        if node.delete:
-            log.info(
-                f"Node {node.qualified_name()} will be deleted. Please note, that if {node.qualified_name()} "
-                f"is a branch all subsequent nodes will also be deleted irrespective of their 'delete' value."
-            )
-            node.parent = None
-            node.children = []
-        node.verify_attributes(abort_on_unknown_attribute)
-        for child in node.children:
-            verify_mandatory_attributes(child, abort_on_unknown_attribute)
-
-
-#
-# Verify that we are using correct YAML in the model
-#
-def check_yaml_usage(flat_model, file_name):
-    for elem in flat_model:
-        if isinstance(elem, list):
-            raise VSpecError(
-                file_name,
-                0,
-                "Element {} is not a list entry. (Did you forget a ':'?)".format(elem))
-
-    # FIXME:
-    # Add more usage checks, such as absence of nested models.
-    # and mutually exclusive elements.
-
-
-# Expand yaml include elements (inserted by yamilify_include())
-#
-def expand_includes(flat_model, prefix, include_paths, tree_type: VSSTreeType):
-    # Build up a new spec model based on the old one, but
-    # with expanded include directives.
-
-    new_flat_model: List[VSSNode] = []
-
-    # Traverse the flat list of the parsed specification
-    for elem in flat_model:
-        # Is this an include element?
-        if elem['$name$'][0:9] == "$include$":
-            include_prefix = elem.get("prefix", "")
-
-            if include_prefix != "":
-                prefix_found = False
-                for dict_elem in new_flat_model:
-                    if dict_elem["$name$"] == include_prefix:
-                        prefix_found = True
-                        break
-                if not prefix_found:
-                    # Printing line number does not make sense, as we work on a
-                    # modified tree
-                    log.warning(
-                        f"No branch matching prefix {include_prefix} for #include in {elem['$file_name$']}")
-
-            # Append include prefix to our current prefix.
-            # Make sure we do not start new prefix with a "."
-            if prefix != "":
-                if include_prefix != "":
-                    include_prefix = "{}.{}".format(prefix, include_prefix)
-                else:
-                    include_prefix = prefix
-
-            # Recursively load included file
-            inc_elem = load_flat_model(
-                elem["file"], include_prefix, include_paths, tree_type)
-
-            # Add the loaded elements at the end of the new spec model
-            new_flat_model.extend(inc_elem)
-        else:
-            # Add a prefix to the element
-            elem["$prefix$"] = prefix
-            # Add the existing elements at the end of the new spec model
-            new_flat_model.append(elem)
-
-    return new_flat_model
-
-
-def expand_tree_instances(tree: VSSNode):
-    tree_node: VSSNode
-
-    def rollout_list(instance_entry):
-        '''
-        Converts "Prefix[1,n] to [Prefix1, Prefix2, ..., Prefixn]"
-        '''
-        prefix = ""
-        if "[" in instance_entry:  # if so unroll
-            unrolled_items = []
-            prefix = instance_entry[:instance_entry.find("[")]
-            start = instance_entry[instance_entry.find(
-                "[") + 1:instance_entry.find(",")]
-            end = instance_entry[instance_entry.find(
-                ",") + 1:instance_entry.find("]")]
-            for i in range(int(start), int(end) + 1):
-                unrolled_items.append(f"{prefix}{i}")
-        else:  # if not, add
-            unrolled_items = instance_entry
-        return unrolled_items, prefix
-
-    def is_instance_branch(node, unrolled_instances, prefix):
-        '''
-        Check if node is a branch that has the same name as an instance for parent node
-        '''
-        for instance in unrolled_instances:
-            if isinstance(instance, list):
-                # This means the element of the instances is a list, e.g. it was
-                # something like Row[1,4]
-                for element in instance:
-                    if element == node.name:
-                        return True
-            else:
-                # in this case our instances have been a simple list of elements
-                # (as opposed to a list of lists), e.g. ['Left', 'Right'], so we just compare by name
-                if instance == node.name:
-                    return True
-
-        # Now we try to be smarter - check if it seems to be an instance by prefix, e.g. Pos3 and prefix is Pos
-        # This is only for specifying instances outside specified range, e.g.
-        # specifying Row5 while instance is specified as Row[1,4]
-        if prefix != "" and prefix in node.name:
-            number = node.name.split(prefix, 1)[1]
-            return number.isnumeric()
-        return False
-
-    def create_instantiated_branch(branch_name, parent, nodes_to_expand):
-        # Check if the branch we want to create as part of expansion (e.g.
-        # Row1, Pos2, Left, ...) already exist
-        old_node = None
-        for child in parent.children:
-            if child.name == branch_name:
-                old_node = child
-        instantiated_branch = VSSNode(
-            branch_name,
-            # autopep8: off
-            {"type": "branch",
-             "description": parent.description,
-             "comment": parent.comment,
-             "$file_name$": "Generated"
-             },
-            # autopep8: on
-            VSSTreeType.SIGNAL_TREE.available_types(),
-            parent)
-        if old_node is not None:
-            # If it exist we take the new one as default (to give e.g. default descriptions and comments)
-            # Then merge anything from the old (expanded) instance above, to get e.g. updated comment
-            # Finally remove the old node by removing parent
-            instantiated_branch.merge(old_node)
-            for child in old_node.children:
-                child.parent = instantiated_branch
-            old_node.parent = None
-
-        # Deep copy needed so that we can change attributes/dict/children
-        # independently
-        for expand_node in deepcopy(nodes_to_expand):
-            # Check if this branch/signal already exists in the instantiated
-            # branch
-            for existing_item in instantiated_branch.children:
-                if expand_node.name == existing_item.name:
-                    # A child with the same name already exists
-                    # Typical use-case is that a single instance of this signal has been re-defined in an overlay
-                    # Then data from the overlay (for example A.B.Row2.Column2.Sig)
-                    # shall have precedence over the expanded instance
-                    # This is handled by removing the old node from tree and
-                    # instead merging it to the new node
-                    expand_node.merge(existing_item)
-                    # Also make sure that any children already existing are moved or merged to the new tree
-
-                    def merge_children(target_node, input_node):
-                        for existing_child in input_node.children:
-                            found = False
-                            for expanded_child in target_node.children:
-                                if expanded_child.name == existing_child.name:
-                                    expanded_child.merge(existing_child)
-                                    merge_children(expanded_child, existing_child)
-                                    found = True
-                            if not found:
-                                # Totally new node/branch, no need to continue deeper, just take it
-                                existing_child.parent = target_node
-                        input_node.parent = None
-
-                    merge_children(expand_node, existing_item)
-                    break
-            expand_node.parent = instantiated_branch
-        return instantiated_branch
-
-    # Checking each node for instances and expand them
-    # The walking order makes sure, we do not need to recurse
-    for tree_node in PreOrderIter(tree):
-        if tree_node.has_instances():
-            # print(f"This node has instances: {tree_node.qualified_name()}, they are *{tree_node.instances}*")
-
-            # Instances can be  many things: A string Row[1,4] that is shorthand for a list,
-            # a simple list of of strings  ['Left', 'Right'],
-            # or a list of lists ['Row[1,4]', ['Left', 'Right']], which expresses
-            # multidimensional instances, where the n'th entry will be expanded at the
-            # n'th level under the current node, i.e. in the example above you expect
-            # children branches Row1, Row2, Row3, Row4, where each of them has a "left"
-            # and a "right" child.
-            # See
-            # https://covesa.github.io/vehicle_signal_specification/rule_set/instances/
-            # for an explanation.
-            # This is a bit painful
-
-            unrolled_instances = []
-            array_prefix = ""
-
-            # Instances are a list in .vspec e.g. ["left", "right"]
-            if isinstance(tree_node.instances, list):
-                for instance_entry in tree_node.instances:
-                    # check every entry whether it is shorthand for a list
-                    # e.g. Prefix[1,3]
-                    unrolled_items, tmpprefix = rollout_list(instance_entry)
-                    if array_prefix == "":
-                        # For "smart" comparison only use prefix from first
-                        # level
-                        array_prefix = tmpprefix
-                    unrolled_instances.append(unrolled_items)
-
-            # it is not a list, e.g. instance in vspec is just Sensor[1,10]
-            else:
-                unrolled_items, array_prefix = rollout_list(
-                    tree_node.instances)
-                unrolled_instances.append(unrolled_items)
-
-            # When a node has instances we need to decide what to do with the children
-            # The default behavior is to duplicate each child under each created instance,
-            # but there are exceptions.
-            #
-            # The first exception is nodes marked in vspec to be excluded from instantiation
-            # (instantiate: False)
-            #
-            # The other exception is nodes/branches that actually already are expanded.
-            # This shall typically only occur when analyzing overlays, where you may find
-            # a signal specified as e.g. Vehicle.Cabin.Door.Row2.Right.Window.Tint
-            # which then shall not be copied for each instance of Door as it is valid only
-            # for one of the instances
-
-            nodes_to_stay = []  # < nodes excluded from instantiation
-            nodes_to_expand = []  # < nodes shall use the instances as parent
-
-            for child in tree_node.children:
-                if is_instance_branch(child, unrolled_instances, array_prefix):
-                    # Child is already an instance, for example Row2.Right.X
-                    nodes_to_stay.append(child)
-                elif child.is_instantiated():
-                    # Child has an explicit or implicit "instantiate:true",
-                    # this is the default
-                    nodes_to_expand.append(child)
-                else:
-                    nodes_to_stay.append(child)
-
-            tree_node.children = deepcopy(nodes_to_stay)
-
-            # now iterate over instances
-            for instance in unrolled_instances:
-                if isinstance(instance, list):
-                    # This means the element of the instances is a list, e.g. it was
-                    # something like Row[1,4]
-                    # In that case the expectation is, that further elements in the
-                    # unrolled instances list will be expanded under it.
-                    # We will only expand the first layer, and come back later, e.g.
-                    # instances are ['Row[1,2]', 'Pos[1,3]']. In that case we will
-                    # add Row1 and Row2 branches under the current node and duplicate
-                    # the subtree, and we set instances property of the new "RowX"
-                    # branches to 'Pos[1,3]'
-                    # The PreOrderIter will pass the newly created "RowX" instances
-                    # next so they will be expanded
-
-                    for element in instance:
-                        instantiated_branch = create_instantiated_branch(
-                            element, tree_node, nodes_to_expand)
-
-                        if len(unrolled_instances) > 1:
-                            # We need to expand more (see above). PreOrderIter
-                            # allows us to do this without recursing
-                            instantiated_branch.instances = unrolled_instances[1:]
-                    # break outer for loop,
-                    break
-
-                else:
-                    # in this case our instances have been a simple list of elements
-                    # (as opposed to a list of lists), e.g. ['Left', 'Right'], so we
-                    # just add them all in parallel as childs of the current
-                    # loop
-                    create_instantiated_branch(
-                        instance, tree_node, nodes_to_expand)
-
-    # As instance expansions moves signals in the tree, we need to recreate
-    # UUIDs
-    create_tree_uuids(tree)
-
-
-#
-# Take the flat model created by _load() and merge all $prefix$ with its name
-# I.e: $prefix$ = "Cabin.Doors.1"
-#      name = "Window.Pos"
-#      -> name = "Cabin.Doors.1.Window.Pos"
-#
-# $prefix$ is deleted
-#
-#
-def create_absolute_paths(flat_model):
-    for elem in flat_model:
-        # Create a list of path components to the given element
-        #
-        # $prefix$='body.door.front.left' name='lock' ->
-        # [ 'body', 'door', 'front', 'left', 'lock' ]
-        name = elem['$name$']
-
-        if elem["$prefix$"] == "":
-            new_name = name
-        else:
-            new_name = "{}.{}".format(elem["$prefix$"], name)
-
-        elem['$name$'] = new_name
-        del elem["$prefix$"]
-
-    return flat_model
-
-
-#
-# Take the flat model with absolute signal names parsed from the vspec
-# file and create a nested variant where each component of a prefix
-# becomes a branch.
-#
-
-def create_nested_model(flat_model, file_name):
-    deep_model = {
-        "children": {},
-        "type": "branch",
-        "$file_name$": file_name,
-        "$line$": 0
-    }
-
-    # Traverse the flat list of the parsed specification
-    for elem in flat_model:
-        # Create child part regardless of type
-        elem["children"] = {}
-
-        # Create a list of path components to the given element
-        #  name='body.door.front.left.lock' ->
-        # [ 'body', 'door', 'front', 'left', 'lock' ]
-        name_list = elem['$name$'].split(".")
-
-        # Extract name
-        name = name_list[-1]
-
-        # Locate the correct branch in the tree
-        parent_branch = find_branch_or_struct(deep_model, name_list[:-1], 0)
-
-        # If an element with name is already in the parent branch
-        # we update its fields with the fields from the new element
-        if name in parent_branch["children"]:
-            old_elem = parent_branch["children"][name]
-            # never update the type
-            elem.pop("type", None)
-            # concatenate file names
-            fname = "{}:{}".format(
-                old_elem["$file_name$"], elem["$file_name$"])
-            old_elem.update(elem)
-            old_elem["$file_name$"] = fname
-        else:
-            parent_branch["children"][name] = elem
-
-    return deep_model
-
-
-# Find the given prefix somewhere under the tree rooted in elem.
-# If the branch referred to by prefix is not found, create it implicitly
-# if autocreate is True. Note that structs are not auto-created.
-def find_branch_or_struct(elem, name_list, index, autocreate=True):
-    # Have we reached the end of the name list
-    # TODO: Add test that verifies that we get an error later instead
-    if len(name_list) == index:
-
-        return elem
-
-    children = elem["children"]
-
-    if name_list[index] not in children:
-        if autocreate and elem['type'] != "struct":
-            log.info(f"Autocreating implicit branch {name_list[index]}")
-
-            # If we are above Vehicle (e.g. vehicle not defined), we are
-            # missing a name
-            if "$name$" not in elem:
-                elem['$name$'] = ""
-            # autopep8: off
-            newelem = {'type': elem['type'],
-                       'children': {},
-                       '$line$': '0',
-                       '$file_name$':
-                       '<generated>',
-                       '$name$': f"{elem['$name$']}.{name_list[index]}"}
-            # autopep8: on
-            children[name_list[index]] = newelem
-            # Search again
-            find_branch_or_struct(elem, name_list, index, autocreate)
-        else:
-            raise VSpecError(
-                elem.get(
-                    "$file_name$", "??"), elem.get(
-                    "$line$", "??"), "Missing branch: {} in {}.".format(
-                    name_list[index], list_to_path(name_list)))
-
-    # Traverse all children, looking for the
-    # Move on to next element in prefix.
-    return find_branch_or_struct(
-        children[name_list[index]], name_list, index + 1, autocreate)
-
-
-def list_to_path(name_list):
-    path = ""
-    for name in name_list:
-        if path == "":
-            path = name
-        else:
-            path = "{}.{}".format(path, name)
-
-    return path
-
-
-#
-# Convert
-#   #include door.vspec, body.door.front.left
-# to
-#   - $include$:
-#     file: door.vspec
-#     prefix: body.door.front.left
-#
-# This yaml version of the include directive will
-# then be further processed to actually include
-# the given file.
-#
-def yamilify_includes(text, is_list):
-
-    # Logic below expects a new line after "#include", to support vspec files where there
-    # is no newline at end we add a newline here
-    text += '\n'
-
-    while True:
-        st_index = text.find("\n#include")
-        if st_index == -1:
-            return text
-
-        end_index = text.find("\n", st_index + 1)
-        if end_index == -1:
-            return text
-
-        include_arg = text[st_index + 10:end_index].split()
-        if len(include_arg) == 2:
-            [include_file, include_prefix] = include_arg
-        else:
-            include_prefix = '""'
-            [include_file] = include_arg
-
-        if is_list:
-            fmt_str = """{}
-
-- $name$: $include$
-  file: {}
-  prefix: {}
-{}"""
-        else:
-            fmt_str = """{}
-
-$include$:
-  file: {}
-  prefix: {}
-{}"""
-
-        text = fmt_str.format(
-            text[:st_index], include_file, include_prefix, text[end_index:])
-
-    return text
-
-
-def render_tree(
-        tree_dict,
-        tree_type: VSSTreeType,
-        break_on_name_style_violation=False) -> VSSNode:
-    if len(tree_dict) != 1:
-        for item in tree_dict.keys():
-            log.info(f"Found root node {item}")
-        raise Exception(
-            f"Invalid VSS model, must have single root node, found {len(tree_dict)}")
-
-    root_element_name = next(iter(tree_dict.keys()))
-    root_element = tree_dict[root_element_name]
-
-    tree_root = VSSNode(
-        root_element_name,
-        root_element,
-        tree_type.available_types(),
-        break_on_name_style_violation=break_on_name_style_violation)
-
-    if tree_root.type is not VSSType.BRANCH:
-        log.error("Root node %s is not of branch type", tree_root.qualified_name())
-        sys.exit(-1)
-
-    if "children" in root_element.keys():
-        child_nodes = root_element["children"]
-        render_subtree(
-            child_nodes,
-            tree_type,
-            tree_root,
-            break_on_name_style_violation=break_on_name_style_violation)
-
-    create_tree_uuids(tree_root)
-    return tree_root
-
-
-def render_subtree(
-        subtree,
-        tree_type: VSSTreeType,
-        parent,
-        break_on_name_style_violation=False):
-    if parent.type not in [VSSType.BRANCH, VSSType.STRUCT]:
-        give_err = False
-        for element_name in subtree:
-            log.warning("Node %s defined in wrong scope", element_name)
-            give_err = True
-        if give_err:
-            log.error("VSS Node %s cannot have children", parent.qualified_name())
-            sys.exit(-1)
-    for element_name in subtree:
-        current_element = subtree[element_name]
-
-        try:
-            new_element = VSSNode(
-                element_name,
-                current_element,
-                tree_type.available_types(),
-                parent=parent,
-                break_on_name_style_violation=break_on_name_style_violation)
-        except IncompleteElementException as e:
-            log.error(f"Invalid VSS: {e}")
-            log.error("Terminating.")
-            sys.exit(-1)
-        if "children" in current_element.keys():
-            child_nodes = current_element["children"]
-            render_subtree(
-                child_nodes,
-                tree_type,
-                new_element,
-                break_on_name_style_violation=break_on_name_style_violation)
-
-
-def merge_elem(base, overlay_element):
-    r = Resolver()
-    element_name = "/" + overlay_element.qualified_name("/")
-    if not VSSNode.node_exists(base, element_name):
-        # The node in the overlay does not exist, so we connect it
-        # print(f"Not exists {overlay_element.qualified_name()} does not exist, creating.")
-        new_parent_name = "/" + overlay_element.parent.qualified_name("/")
-        new_parent = r.get(base, new_parent_name)
-        overlay_element.parent = new_parent
-
-    else:
-        # else we merge the node. The merge function of VSSNode is not recursive
-        # so children in base will not be overwritten
-        # print(f"Merging {overlay_element.qualified_name()}")
-        other_node: VSSNode = r.get(base, element_name)
-        try:
-            other_node.merge(overlay_element)
-
-        except ImpossibleMergeException as e:
-            log.error(f"Merging impossible: {e}")
-            sys.exit(-1)
-
-
-def merge_tree(base: VSSNode, overlay: VSSNode):
-    overlay_element: VSSNode
-    for overlay_element in LevelOrderIter(overlay):
-        merge_elem(base, overlay_element)
-
-
-def create_tree_uuids(root: VSSNode):
-    VSS_NAMESPACE = "vehicle_signal_specification"
-    namespace_uuid = uuid.uuid5(uuid.NAMESPACE_OID, VSS_NAMESPACE)
-    vss_element: VSSNode
-    for vss_element in PreOrderIter(root):
-        vss_element.uuid = uuid.uuid5(
-            namespace_uuid, vss_element.qualified_name()).hex
-
-
-def load_quantities(vspec_file: str, quantity_files: List[str]):
-    """
-    Reset the list of quantities defined (if any)
-    and add new quantities
-    """
-    VSSQuantityCollection.reset_quantities()
-
-    total_nbr_quantities = 0
-    if not quantity_files:
-        # Search for a file quantities.yaml in same directory as vspec file
-        vspec_dir = os.path.dirname(os.path.realpath(vspec_file))
-        default_vss_quantity_file = vspec_dir + os.path.sep + 'quantities.yaml'
-        if os.path.exists(default_vss_quantity_file):
-            total_nbr_quantities = VSSQuantityCollection.load_config_file(default_vss_quantity_file)
-            log.info(f"Added {total_nbr_quantities} quantities from {default_vss_quantity_file}")
-    else:
-        for quantity_file in quantity_files:
-            nbr_quantities = VSSQuantityCollection.load_config_file(quantity_file)
-            if (nbr_quantities == 0):
-                log.warning(f"Warning: No quantities found in {quantity_file}")
-            else:
-                log.info(f"Added {nbr_quantities} quantities from {quantity_file}")
-                total_nbr_quantities += nbr_quantities
-
-    if (total_nbr_quantities == 0):
-        log.info("No quantities defined!")
-
-
-def load_units(vspec_file: str, unit_files: List[str]):
-    """
-    Reset the list of units defined (if any)
-    and add new units
-    """
-    VSSUnitCollection.reset_units()
-
-    total_nbr_units = 0
-    if not unit_files:
-        # Search for a file units.yaml in same directory as vspec file
-        vspec_dir = os.path.dirname(os.path.realpath(vspec_file))
-        default_vss_unit_file = vspec_dir + os.path.sep + 'units.yaml'
-        if os.path.exists(default_vss_unit_file):
-            total_nbr_units = VSSUnitCollection.load_config_file(default_vss_unit_file)
-            log.info(f"Added {total_nbr_units} units from {default_vss_unit_file}")
-    else:
-        for unit_file in unit_files:
-            nbr_units = VSSUnitCollection.load_config_file(unit_file)
-            if (nbr_units == 0):
-                log.warning(f"Warning: No units found in {unit_file}")
-            else:
-                log.info(f"Added {nbr_units} units from {unit_file}")
-                total_nbr_units += nbr_units
-
-    if (total_nbr_units == 0):
-        log.error("No units defined! Terminating!")
-        sys.exit(-1)
-
-
-def check_data_type_references(tree: VSSNode):
-    """
-    Check that the data type names referenced by tree nodes exist in the tree.
-    """
-    errors: List[str] = []
-    check_data_type_references_recursive(
-        tree, [
-            str(attr) for attr in VSSNode.get_tree_attrs(
-                tree, lambda n: n.qualified_name(), lambda n: n.is_struct())], errors)
-    if len(errors) > 0:
-        error_str = ",".join(errors)
-        log.error(
-            f"{len(errors)} data type reference errors detected. Errors: {error_str}")
-        sys.exit(-1)
-
-
-def check_data_type_references_recursive(
-        node: VSSNode, data_type_qualified_names: List[str], errors: List[str]):
-    """
-    Check that the data type names referenced by tree nodes exist in the tree.
-    """
-    log.info("jkdfjk %s", node.qualified_name())
-    if node.is_property() or node.is_signal():
-        log.info("Node is %s", node.qualified_name())
-        if node.children:
-            # Exit directly if node defined in faulty scope
-            log.error("Node %s is not defined in a branch", node.qualified_name())
-            sys.exit(-1)
-        if node.datatype is None:
-            undecorated_data_type = node.data_type_str.replace('[]', '')
-            if undecorated_data_type not in data_type_qualified_names:
-                errors.append(
-                    f"Data Type reference invalid. Node Name: {node.name}. "
-                    f"Node Qualified Name: {node.qualified_name()}. "
-                    f"Data Type: {undecorated_data_type}")
-
-            separator = "."
-            # is the property circularly refereing to the struct in which it is defined?
-            member_of = separator.join(node.qualified_name(separator).split(separator)[:-1])
-            if member_of == undecorated_data_type:
-                errors.append(
-                    "Circular reference detected in data type. "
-                    f"Data Type: {member_of}. Property: {node.name}")
-
-    for n in node.children:
-        check_data_type_references_recursive(
-            n, data_type_qualified_names, errors)
-
-
-def check_data_type_references_across_trees(
-        signal_root: VSSNode, data_type_root: Optional[VSSNode]):
-    """
-    Check that the data type names referenced by nodes in the signal tree are present in the data types tree.
-    """
-    nonexistant_types = []
-    for _, _, node in RenderTree(signal_root):
-        # signals with user-defined data types
-        if node.is_signal() and node.datatype is None:
-            if data_type_root is None or (not node.does_attribute_exist(
-                    data_type_root,
-                    lambda n: n.base_data_type_str(),
-                    lambda n: n.qualified_name(),
-                    lambda n: n.is_struct())):
-                nonexistant_types.append(f'{node.data_type_str}')
-
-    if len(nonexistant_types) > 0:
-        error_str = ", ".join(nonexistant_types)
-        log.error(
-            f"Following types were referenced in signals but have not been defined: {error_str}")
-        sys.exit(-1)
-
-
-load_flat_model.include_index = 1  # type: ignore[attr-defined]
diff --git a/src/vss_tools/vspec/cli.py b/src/vss_tools/vspec/cli.py
new file mode 100644
index 00000000..02e5583d
--- /dev/null
+++ b/src/vss_tools/vspec/cli.py
@@ -0,0 +1,59 @@
+# Copyright (c) 2023 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+
+import logging
+import rich_click as click
+from vss_tools.vspec.lazy_group import LazyGroup
+import vss_tools.vspec.cli_options as clo
+from vss_tools import log
+from pathlib import Path
+
+
+@click.group(
+    context_settings={"auto_envvar_prefix": "vss_tools"}, invoke_without_command=True
+)
+@clo.log_level_opt
+@clo.log_file_opt
+@click.version_option()
+@click.pass_context
+def cli(ctx: click.Context, log_level: str, log_file: Path):
+    if ctx.invoked_subcommand is None:
+        click.echo(ctx.get_help())
+    if log_file:
+        file_handler = logging.FileHandler(log_file, mode="w")
+        file_handler.setFormatter(
+            logging.Formatter("%(asctime)s:%(levelname)s:%(message)s")
+        )
+        log.addHandler(file_handler)
+
+    log.setLevel(log_level)
+
+
+@cli.group(
+    cls=LazyGroup,
+    lazy_subcommands={
+        "binary": "vss_tools.vspec.vssexporters.vss2binary:cli",
+        "csv": "vss_tools.vspec.vssexporters.vss2csv:cli",
+        "ddsidl": "vss_tools.vspec.vssexporters.vss2ddsidl:cli",
+        "franca": "vss_tools.vspec.vssexporters.vss2franca:cli",
+        "graphql": "vss_tools.vspec.vssexporters.vss2graphql:cli",
+        "id": "vss_tools.vspec.vssexporters.vss2id:cli",
+        "json": "vss_tools.vspec.vssexporters.vss2json:cli",
+        "jsonschema": "vss_tools.vspec.vssexporters.vss2jsonschema:cli",
+        "protobuf": "vss_tools.vspec.vssexporters.vss2protobuf:cli",
+        "yaml": "vss_tools.vspec.vssexporters.vss2yaml:cli",
+        "tree": "vss_tools.vspec.vssexporters.vss2tree:cli",
+    },
+)
+@click.pass_context
+def export(ctx: click.Context):
+    """
+    Export a vspec to a chosen format
+    """
+    if ctx.invoked_subcommand is None:
+        click.echo(ctx.get_help())
diff --git a/src/vss_tools/vspec/cli_options.py b/src/vss_tools/vspec/cli_options.py
index a7d6da1e..ac660b74 100644
--- a/src/vss_tools/vspec/cli_options.py
+++ b/src/vss_tools/vspec/cli_options.py
@@ -9,14 +9,18 @@
 import rich_click as click
 from rich_click import option
 from pathlib import Path
+from vss_tools.vspec.model import get_all_model_fields
 
 
-def no_colon_type(value):
+def validate_attribute(value):
     """
-    Give error if comma is used in the argument
+    Checks that a user given attribute is valid.
+    Should not contain ',' and should not be a core attribute
     """
     if "," in value:
         raise click.BadParameter("Comma (',') not allowed")
+    if value in get_all_model_fields():
+        raise click.BadParameter(f"'{value}' is a core attribute")
     return value
 
 
@@ -48,15 +52,16 @@ def no_colon_type(value):
     "--extended-attributes",
     "-e",
     multiple=True,
+    type=validate_attribute,
     help="Whitelisted extended attributes",
-    type=no_colon_type,
 )
 
 strict_opt = option(
     "--strict/--no-strict",
     help="Whether to enable strict. Enables all '--abort/-a' values.",
     default=False,
-    show_default=True)
+    show_default=True,
+)
 
 aborts_opt = option(
     "--aborts",
@@ -67,10 +72,15 @@ def no_colon_type(value):
     show_choices=True,
 )
 
-uuid_opt = option("--uuid/--no-uuid", help="Whether to add UUIDs.", show_default=True, default=False)
+uuid_opt = option(
+    "--uuid/--no-uuid", help="Whether to add UUIDs.", show_default=True, default=False
+)
 
 expand_opt = option(
-    "--expand/--no-expand", default=True, show_default=True, help="Whether to expand 'instances'."
+    "--expand/--no-expand",
+    default=True,
+    show_default=True,
+    help="Whether to expand 'instances'.",
 )
 
 overlays_opt = option(
@@ -102,7 +112,7 @@ def no_colon_type(value):
     "-s",
     type=click.Path(dir_okay=False, readable=True, path_type=Path, exists=True),
     required=True,
-    help="The vspec file."
+    help="The vspec file.",
 )
 
 output_required_opt = option(
@@ -110,7 +120,14 @@ def no_colon_type(value):
     "-o",
     type=click.Path(dir_okay=False, writable=True, path_type=Path),
     help="Output file.",
-    required=True
+    required=True,
+)
+
+output_opt = option(
+    "--output",
+    "-o",
+    type=click.Path(dir_okay=False, writable=True, path_type=Path),
+    help="Output file.",
 )
 
 types_opt = option(
@@ -141,7 +158,9 @@ def no_colon_type(value):
         not only the ones given via '-e/--extended-attributes'
     """,
     default=False,
-    show_default=True
+    show_default=True,
 )
 
-pretty_print_opt = option("--pretty/--no-pretty", help="Pretty print.", default=False, show_default=True)
+pretty_print_opt = option(
+    "--pretty/--no-pretty", help="Pretty print.", default=False, show_default=True
+)
diff --git a/src/vss_tools/vspec/datatypes.py b/src/vss_tools/vspec/datatypes.py
new file mode 100644
index 00000000..3419a4ae
--- /dev/null
+++ b/src/vss_tools/vspec/datatypes.py
@@ -0,0 +1,297 @@
+# Copyright (c) 2023 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+from typing import Any, Callable, Set
+from vss_tools import log
+
+# Global objects to be extended by other code parts
+dynamic_datatypes: Set[str] = set()
+dynamic_quantities: list[str] = []
+# This one contains the unit name as well as the list of allowed-datatypes
+dynamic_units: dict[str, list] = {}
+
+
+class DatatypesException(Exception):
+    pass
+
+
+def is_xintx(value: Any, signed: bool, bits: int):
+    values = [value]
+    if isinstance(value, list):
+        values = value
+
+    for v in values:
+        if not isinstance(v, int):
+            return False
+        if not signed and v < 0:
+            return False
+        max = 2**bits - 1
+        min = 0
+        if signed:
+            max = 2 ** (bits - 1) - 1
+            min = -(2 ** (bits - 1))
+        if not (v <= max and v >= min):
+            return False
+    return True
+
+
+def is_uint8(value: Any) -> bool:
+    return is_xintx(value, False, 8)
+
+
+def is_int8(value: Any) -> bool:
+    return is_xintx(value, True, 8)
+
+
+def is_uint16(value: Any) -> bool:
+    return is_xintx(value, False, 16)
+
+
+def is_int16(value: Any) -> bool:
+    return is_xintx(value, True, 16)
+
+
+def is_uint32(value: Any) -> bool:
+    return is_xintx(value, False, 32)
+
+
+def is_int32(value: Any) -> bool:
+    return is_xintx(value, True, 32)
+
+
+def is_uint64(value: Any) -> bool:
+    return is_xintx(value, False, 64)
+
+
+def is_int64(value: Any) -> bool:
+    return is_xintx(value, True, 64)
+
+
+def is_x(value: Any, c: type) -> bool:
+    values = [value]
+    if isinstance(value, list):
+        values = value
+    return all([isinstance(v, c) for v in values])
+
+
+def is_bool(value: Any) -> bool:
+    return is_x(value, bool)
+
+
+def is_float(value: Any) -> bool:
+    return is_x(value, float) or is_x(value, int)
+
+
+def is_string(value: Any) -> bool:
+    return is_x(value, str)
+
+
+def is_int(value: Any) -> bool:
+    return is_x(value, int)
+
+
+def is_numeric(value: Any) -> bool:
+    return is_int(value) or is_float(value)
+
+
+class Datatypes:
+    # Types contain a tuple of the type name, the validation function whether
+    # a value of type Any is a certain datatype
+    # The last entry is a list of included subtypes
+    # E.g. a unit8 is a valid uint16 usw..
+    UINT8: tuple[str, Callable[[Any], bool], list[str]] = "uint8", is_uint8, []
+    UINT8_ARRAY: tuple[str, Callable[[Any], bool], list[str]] = "uint8[]", is_uint8, []
+    INT8: tuple[str, Callable[[Any], bool], list[str]] = "int8", is_int8, []
+    INT8_ARRAY: tuple[str, Callable[[Any], bool], list[str]] = "int8[]", is_int8, []
+    UINT16 = "uint16", is_uint16, ["uint8"]
+    UINT16_ARRAY = "uint16[]", is_uint16, ["uint8[]"]
+    INT16 = "int16", is_int16, ["int8"]
+    INT16_ARRAY = "int16[]", is_int16, ["int8[]"]
+    UINT32 = "uint32", is_uint32, ["uint16", "uint8"]
+    UINT32_ARRAY = "uint32[]", is_uint32, ["uint16[]", "uint8[]"]
+    INT32 = "int32", is_int32, ["int16", "int8"]
+    INT32_ARRAY = "int32[]", is_int32, ["int16[]", "int8[]"]
+    UINT64 = "uint64", is_uint64, ["uint32", "uint16", "uint8"]
+    UINT64_ARRAY = "uint64[]", is_uint64, ["uint32[]", "uint16[]", "uint8[]"]
+    INT64 = "int64", is_int64, ["int32", "int16", "int8"]
+    INT64_ARRAY = "int64[]", is_int64, ["int32[]", "int16[]", "int8[]"]
+    BOOLEAN: tuple[str, Callable[[Any], bool], list[str]] = "boolean", is_bool, []
+    BOOLEAN_ARRAY: tuple[str, Callable[[Any], bool], list[str]] = (
+        "boolean[]",
+        is_bool,
+        [],
+    )
+    FLOAT: tuple[str, Callable[[Any], bool], list[str]] = "float", is_float, []
+    FLOAT_ARRAY: tuple[str, Callable[[Any], bool], list[str]] = "float[]", is_float, []
+    DOUBLE: tuple[str, Callable[[Any], bool], list[str]] = "double", is_float, []
+    DOUBLE_ARRAY: tuple[str, Callable[[Any], bool], list[str]] = (
+        "double[]",
+        is_float,
+        [],
+    )
+    STRING: tuple[str, Callable[[Any], bool], list[str]] = "string", is_string, []
+    STRING_ARRAY: tuple[str, Callable[[Any], bool], list[str]] = (
+        "string[]",
+        is_string,
+        [],
+    )
+    NUMERIC = (
+        "numeric",
+        is_numeric,
+        [
+            "int64",
+            "int32",
+            "int16",
+            "int8",
+            "uint64",
+            "uint32",
+            "uint16",
+            "uint8",
+            "float",
+            "double",
+        ],
+    )
+    NUMERIC_ARRAY = (
+        "numeric[]",
+        is_numeric,
+        [
+            "int64[]",
+            "int32[]",
+            "int16[]",
+            "int8[]",
+            "uint64[]",
+            "uint32[]",
+            "uint16[]",
+            "uint8[]",
+            "float[]",
+            "double[]",
+        ],
+    )
+    types = [
+        UINT8,
+        UINT8_ARRAY,
+        INT8,
+        INT8_ARRAY,
+        UINT16,
+        UINT16_ARRAY,
+        INT16,
+        INT16_ARRAY,
+        UINT32,
+        UINT32_ARRAY,
+        INT32,
+        INT32_ARRAY,
+        UINT64,
+        UINT64_ARRAY,
+        INT64,
+        INT64_ARRAY,
+        BOOLEAN,
+        BOOLEAN_ARRAY,
+        FLOAT,
+        FLOAT_ARRAY,
+        DOUBLE,
+        DOUBLE_ARRAY,
+        STRING,
+        STRING_ARRAY,
+        NUMERIC,
+        NUMERIC_ARRAY,
+    ]
+
+    @classmethod
+    def get_type(cls, datatype: str) -> tuple[str, Callable, list[str]] | None:
+        for t in cls.types:
+            if datatype == t[0]:
+                return t
+        return None
+
+    @classmethod
+    def is_datatype(cls, value: Any, datatype: str) -> bool:
+        t = cls.get_type(datatype)
+        if t is None:
+            raise DatatypesException(f"Unsupported datatype: {datatype}")
+        return t[1](value)
+
+    @classmethod
+    def is_subtype_of(cls, check: str, base: str) -> bool:
+        check_type = cls.get_type(check)
+        if not check_type:
+            raise DatatypesException(f"Not a valid type: '{check}'")
+        base_type = cls.get_type(base)
+        if not base_type:
+            raise DatatypesException(f"Not a valid type: '{base}'")
+        return check in base_type[2] or check == base
+
+
+def get_fqn_namespaced_datatypes(fqn: str | None = None) -> dict[str, str]:
+    """
+    We want to be able to reference datatypes from within the same file
+    or same namespace since we support structs in structs
+
+    For instance if we have the struct Types.A.B.C
+    and we have another struct Types.A.B.D then the D struct
+    should be able to reference C as a datatype
+    """
+    if not fqn:
+        return {}
+    fqn_namespaced_datatypes = {}
+    for t in dynamic_datatypes:
+        # This excludes referencing the own struct as a datatype
+        if fqn.startswith(t):
+            continue
+
+        # fqn: Types.A.B.C.x_property
+        # t: Types.A.B.D
+        # We are checking whether fqn starts with Types.A.B
+        # in this case we add {D: Types.A.B.D}
+        if fqn.startswith(".".join(t.split(".")[:-1])):
+            fqn_namespaced_datatypes[(t.split(".")[-1])] = t
+
+    if fqn_namespaced_datatypes:
+        log.debug(f"Namespaced datatypes, {fqn=}, {fqn_namespaced_datatypes=}")
+    return fqn_namespaced_datatypes
+
+
+def get_dynamic_datatypes(fqn: str | None = None) -> list[str]:
+    """
+    Aggregator to gather all custom datatypes for a given fqn
+    The fqn is needed to support the namespaced custom datatypes
+    """
+    fqn_namespaced_datatypes = set(get_fqn_namespaced_datatypes(fqn).keys())
+    dynamic_array_datatypes = [
+        f"{t}[]" for t in dynamic_datatypes | fqn_namespaced_datatypes
+    ]
+    return (
+        list(dynamic_datatypes)
+        + dynamic_array_datatypes
+        + list(fqn_namespaced_datatypes)
+    )
+
+
+def get_all_datatypes(fqn: str | None = None) -> list[str]:
+    static_datatypes = [t[0] for t in Datatypes.types]
+    dynamic_datatypes = get_dynamic_datatypes(fqn)
+    return static_datatypes + dynamic_datatypes
+
+
+def resolve_datatype(datatype: str, fqn: str | None) -> str:
+    """
+    Resolves a possible shortened namespaced type into the
+    fully qualified types
+    """
+    if not fqn:
+        return datatype
+
+    array = is_array(datatype)
+    datatype = datatype.rstrip("[]")
+
+    resvoled = get_fqn_namespaced_datatypes(fqn).get(datatype, datatype)
+    if array:
+        resvoled += "[]"
+    return resvoled
+
+
+def is_array(datatype: str) -> bool:
+    return datatype.endswith("[]")
diff --git a/src/vss_tools/vspec/main.py b/src/vss_tools/vspec/main.py
new file mode 100644
index 00000000..8bb92b0b
--- /dev/null
+++ b/src/vss_tools/vspec/main.py
@@ -0,0 +1,271 @@
+# Copyright (c) 2023 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+
+from vss_tools import log
+from anytree import PreOrderIter, findall
+from pathlib import Path
+from vss_tools.vspec.model import (
+    VSSDataBranch,
+    VSSDataProperty,
+    VSSDataStruct,
+    get_all_model_fields,
+)
+from vss_tools.vspec.tree import VSSNode, build_tree, ModelValidationException
+from vss_tools.vspec.vspec import load_vspec, InvalidSpecDuplicatedEntryException
+from vss_tools.vspec.units_quantities import load_quantities, load_units
+from vss_tools.vspec.datatypes import (
+    dynamic_units,
+    dynamic_datatypes,
+    dynamic_quantities,
+)
+
+
+class NameViolationException(Exception):
+    pass
+
+
+class ExtraAttributesException(Exception):
+    pass
+
+
+class MultipleTypeTreesException(Exception):
+    pass
+
+
+class RootTypesException(Exception):
+    pass
+
+
+class PropertyOrphansException(Exception):
+    pass
+
+
+def load_quantities_and_units(
+    quantities: tuple[Path, ...], units: tuple[Path, ...], vspec_root: Path
+) -> None:
+    """
+    Loading quantities and units.
+    Side effect: filling global 'dynamic_quantities' and 'dynamic_units' from 'datatypes'
+    """
+    if not quantities:
+        default_quantity = vspec_root / "quantities.yaml"
+        if default_quantity.exists():
+            quantities = (default_quantity,)
+        else:
+            log.warning(
+                f"No 'quantity' files defined. Default not existing: {default_quantity.absolute()}"
+            )
+    if not units:
+        default_unit = vspec_root / "units.yaml"
+        if default_unit.exists():
+            units = (default_unit,)
+        else:
+            log.warning(
+                f"No 'unit' files defined. Default not existing: {default_unit.absolute()}"
+            )
+
+    quantity_data = load_quantities(list(quantities))
+    dynamic_quantities.extend(list(quantity_data.keys()))
+    unit_data = load_units(list(units))
+    for k, v in unit_data.items():
+        allowed_datatypes = []
+        if v.allowed_datatypes is not None:
+            allowed_datatypes = v.allowed_datatypes
+        if v.unit is not None:
+            dynamic_units[v.unit] = allowed_datatypes
+        dynamic_units[k] = allowed_datatypes
+
+
+def check_name_violations(root: VSSNode, strict: bool, aborts: tuple[str, ...]) -> None:
+    if strict or "name-style" in aborts:
+        naming_violations = root.get_naming_violations()
+        if naming_violations:
+            for violation in naming_violations:
+                log.warning(f"Name violation: '{violation[0]}' ({violation[1]})")
+            raise NameViolationException(
+                f"Name violations detected: {naming_violations}"
+            )
+
+
+def check_extra_attribute_violations(
+    root: VSSNode,
+    strict: bool,
+    aborts: tuple[str, ...],
+    extended_attributes: tuple[str, ...],
+) -> None:
+    extra_attributes = root.get_extra_attributes(extended_attributes)
+    for attribute in extra_attributes:
+        log.warning(f"Unknown extra attribute: '{attribute[0]}':'{attribute[1]}'")
+        if attribute[1] in get_all_model_fields():
+            raise ExtraAttributesException(
+                f"Forbidden extra attribute (core attribute): '{attribute[0]}':'{attribute[1]}'"
+            )
+    if strict or "unknown-attribute" in aborts:
+        if extra_attributes:
+            raise ExtraAttributesException(
+                f"Forbidden extra attributes detected: {extra_attributes}"
+            )
+
+
+def get_types_root(types: tuple[Path, ...], include_dirs: list[Path]) -> VSSNode | None:
+    if not types:
+        log.debug("No user 'types' defined")
+        return None
+
+    types_root: VSSNode | None = None
+    # We are iterating to be able to reference
+    # types from earlier type files
+    for types_file in list(types):
+        data = load_vspec(include_dirs, [types_file], "Types")
+        root, orphans = build_tree(data.data)
+        if orphans:
+            log.error(f"Types model has orphans\n{orphans}")
+            exit(1)
+        if types_root:
+            node: VSSNode
+            for node in PreOrderIter(root):
+                if not types_root.connect(node.get_fqn(), node):
+                    raise MultipleTypeTreesException()
+        else:
+            types_root = root
+
+    if dynamic_datatypes:
+        log.info(f"Dynamic datatypes added={len(dynamic_datatypes)}")
+        log.debug(f"Dynamic datatypes:\n{dynamic_datatypes}")
+
+    # Checking whether user defined root types e.g 'MyType'
+    # instead of 'Types.MyType'
+    if not all(["." in t for t in dynamic_datatypes]):
+        raise RootTypesException()
+
+    if types_root:
+        try:
+            types_root.resolve()
+        except ModelValidationException as e:
+            log.critical(e)
+            exit(1)
+
+    return types_root
+
+
+def get_invalid_node_msgs(root: VSSNode) -> list[str]:
+    """
+    Validating whether tree nodes have correct parents
+    All nodes need 'branch' parents except 'properties'.
+    Properties need a 'struct' as a parent.
+    Returning error msgs
+    """
+    invalid_nodes = []
+    for node in PreOrderIter(root):
+        ok = True
+        if node.parent is None:
+            continue
+        if isinstance(node.data, VSSDataProperty):
+            if not isinstance(node.parent.data, VSSDataStruct):
+                ok = False
+        elif isinstance(node.data, VSSDataStruct):
+            if not isinstance(node.parent.data, VSSDataStruct) and not isinstance(
+                node.parent.data, VSSDataBranch
+            ):
+                ok = False
+        else:
+            if not isinstance(node.parent.data, VSSDataBranch):
+                ok = False
+        if not ok:
+            entry = f"'{node.get_fqn()} ({node.data.__class__.__name__})',"
+            entry += f" invalid parent: '{node.parent.data.__class__.__name__}'"
+            invalid_nodes.append(entry)
+    return invalid_nodes
+
+
+def validate_tree(root: VSSNode) -> None:
+    invalid_node_msgs = get_invalid_node_msgs(root)
+    if invalid_node_msgs:
+        log.critical(f"Invalid nodes={len(invalid_node_msgs)}")
+        for node in invalid_node_msgs:
+            log.critical(node)
+        exit(1)
+
+
+def get_trees(
+    vspec: Path,
+    include_dirs: tuple[Path, ...] = tuple(),
+    aborts: tuple[str, ...] = tuple(),
+    strict: bool = False,
+    extended_attributes: tuple[str, ...] = tuple(),
+    uuid: bool = False,
+    quantities: tuple[Path, ...] = tuple(),
+    units: tuple[Path, ...] = tuple(),
+    types: tuple[Path, ...] = tuple(),
+    overlays: tuple[Path, ...] = tuple(),
+    expand: bool = True,
+) -> tuple[VSSNode, VSSNode | None]:
+    """
+    Loading vspec files, building and validating trees (types and normal).
+    Returning a tuple of the root and the types tree
+    """
+    if extended_attributes:
+        log.info(f"User defined extra attributes: {extended_attributes}")
+    try:
+        load_quantities_and_units(quantities, units, vspec.parent)
+    except ModelValidationException as e:
+        log.critical(e)
+        exit(1)
+
+    unique_include_dirs = []
+    for include_dir in include_dirs:
+        if include_dir not in unique_include_dirs:
+            unique_include_dirs.append(include_dir)
+
+    try:
+        types_root = get_types_root(types, unique_include_dirs)
+        vspec_data = load_vspec(unique_include_dirs, [vspec] + list(overlays))
+    except InvalidSpecDuplicatedEntryException as e:
+        log.critical(e)
+        exit(1)
+
+    root, orphans = build_tree(vspec_data.data, connect_orphans=True)
+
+    if orphans:
+        log.error(f"Model has orphans\n{list(orphans.keys())}")
+        exit(1)
+
+    if expand:
+        root.expand_instances()
+
+    if uuid:
+        root.add_uuids()
+
+    try:
+        root.resolve()
+    except ModelValidationException as e:
+        log.critical(e)
+        exit(1)
+
+    root.delete_nodes(findall(root, filter_=lambda n: n.get_vss_data().delete))
+
+    validate_tree(root)
+
+    if types_root:
+        validate_tree(types_root)
+        try:
+            check_extra_attribute_violations(
+                types_root, True, aborts, extended_attributes
+            )
+        except (NameViolationException, ExtraAttributesException) as e:
+            log.critical(e)
+            exit(1)
+
+    try:
+        check_name_violations(root, strict, aborts)
+        check_extra_attribute_violations(root, strict, aborts, extended_attributes)
+    except (NameViolationException, ExtraAttributesException) as e:
+        log.critical(e)
+        exit(1)
+
+    return root, types_root
diff --git a/src/vss_tools/vspec/model.py b/src/vss_tools/vspec/model.py
new file mode 100644
index 00000000..011b8e3f
--- /dev/null
+++ b/src/vss_tools/vspec/model.py
@@ -0,0 +1,351 @@
+# Copyright (c) 2023 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+from enum import Enum
+from typing import Any
+from typing_extensions import Self
+from rich.pretty import pretty_repr
+from vss_tools import log
+import re
+
+from pydantic import (
+    BaseModel,
+    ValidationError,
+    field_validator,
+    model_validator,
+    Field,
+    ConfigDict,
+)
+
+from vss_tools.vspec.datatypes import (
+    Datatypes,
+    dynamic_quantities,
+    dynamic_units,
+    get_all_datatypes,
+    is_array,
+    resolve_datatype,
+)
+
+EXPORT_EXCLUDE_ATTRIBUTES = ["delete", "instantiate", "fqn", "arraysize", "aggregate"]
+
+
+class ModelException(Exception):
+    pass
+
+
+class ModelValidationException(Exception):
+    """
+    Exception that pretty formats the Pydantic
+    ValidationError
+    """
+
+    def __init__(self, element: str | None, ve: ValidationError):
+        self.element = element
+        self.ve = ve
+
+    def __str__(self) -> str:
+        errors = self.ve.errors(include_url=False)
+        return (
+            f"'{self.element}' has {len(errors)} model error(s):\n{pretty_repr(errors)}"
+        )
+
+
+class NodeType(str, Enum):
+    BRANCH = "branch"
+    ATTRIBUTE = "attribute"
+    SENSOR = "sensor"
+    ACTUATOR = "actuator"
+    STRUCT = "struct"
+    PROPERTY = "property"
+
+
+class VSSRaw(BaseModel):
+    """
+    Most low level model that accepts everything
+    In the end we need that to support the scenario
+    of being able to overlay things that have been
+    unrolled through instances.
+    Not only that but to support defining those elements
+    incomplete in terms of our model.
+    After expanding instances we convert all raw nodes
+    """
+
+    fqn: str | None = None
+
+    model_config = ConfigDict(extra="allow")
+
+    def get_extra_attributes(self) -> list[str]:
+        defined_fields = self.model_fields.keys()
+        additional_fields = set(self.model_dump().keys()) - set(defined_fields)
+        return list(additional_fields)
+
+    def as_dict(
+        self,
+        with_extra_attributes: bool = True,
+        exclude_fields: list[str] = EXPORT_EXCLUDE_ATTRIBUTES,
+    ) -> dict[str, Any]:
+        excludes = exclude_fields.copy()
+        if not with_extra_attributes:
+            excludes.extend(self.get_extra_attributes())
+        data = {
+            k: v
+            for k, v in self.model_dump(
+                mode="json", exclude_none=True, exclude=set(excludes)
+            ).items()
+            if v != []
+        }
+        return data
+
+
+class VSSData(VSSRaw):
+    model_config = ConfigDict(extra="allow")
+    type: NodeType
+    description: str
+    comment: str | None = None
+    delete: bool = False
+    deprecation: str | None = None
+    constUID: str | None = None
+    fka: list[str] = []
+    instantiate: bool = True
+
+    @field_validator("constUID")
+    @classmethod
+    def check_const_uid_format(cls, v: str) -> str:
+        pattern = r"^0x[0-9A-Fa-f]{8}$"
+        assert bool(re.match(pattern, v)), f"'{v}' is not a valid 'constUID'"
+        return v
+
+
+class VSSDataBranch(VSSData):
+    instances: Any = None
+    aggregate: bool = False
+
+    @field_validator("instances")
+    @classmethod
+    def fill_instances(cls, v: Any) -> list[str]:
+        """
+        Instances can be specified as a string or a list
+        and even of a list of lists.
+        Normalizing it to be a list
+        """
+        if not (isinstance(v, str) or isinstance(v, list)):
+            assert False, f"'{v}' is not a valid 'instances' content"
+        if isinstance(v, str):
+            return [v]
+        return v
+
+
+class VSSUnit(BaseModel):
+    definition: str
+    unit: str | None
+    quantity: str
+    allowed_datatypes: list[str] | None = Field(alias="allowed-datatypes", default=None)
+
+    @field_validator("quantity")
+    @classmethod
+    def check_valid_quantity(cls, v: str) -> str:
+        assert v in dynamic_quantities, f"Invalid quantity: '{v}'"
+        return v
+
+    @field_validator("allowed_datatypes")
+    @classmethod
+    def check_valid_datatypes(cls, values: list[str]) -> list[str]:
+        datatypes = get_all_datatypes()
+        for value in values:
+            assert value in datatypes, f"Invalid datatype: '{value}'"
+        return values
+
+
+class VSSQuantity(BaseModel):
+    definition: str
+    comment: str | None = None
+    remark: str | None = None
+
+
+class VSSDataDatatype(VSSData):
+    datatype: str
+    arraysize: int | None = None
+    min: int | float | None = None
+    max: int | float | None = None
+    unit: str | None = None
+    allowed: list[str | int | float | bool] | None = None
+    default: list[str | int | float | bool] | str | int | float | bool | None = None
+
+    @model_validator(mode="after")
+    def check_type_arraysize_consistency(self) -> Self:
+        """
+        Checks that arraysize is only set when
+        datatype is an array
+        """
+        if self.arraysize is not None:
+            assert is_array(
+                self.datatype
+            ), f"'arraysize' set on a non array datatype: '{self.datatype}'"
+        return self
+
+    @model_validator(mode="after")
+    def check_type_default_consistency(self) -> Self:
+        """
+        Checks that the default value
+        is consistent with the given datatype
+        """
+        if self.default is not None:
+            if is_array(self.datatype):
+                assert isinstance(
+                    self.default, list
+                ), f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'"
+                if self.arraysize:
+                    assert (
+                        len(self.default) == self.arraysize
+                    ), "'default' array size does not match 'arraysize'"
+                for v in self.default:
+                    assert Datatypes.is_datatype(
+                        v, self.datatype
+                    ), f"'{v}' is not of type '{self.datatype}'"
+            else:
+                assert not isinstance(
+                    self.default, list
+                ), f"'default' with type '{type(self.default)}' does not match datatype '{self.datatype}'"
+                assert Datatypes.is_datatype(
+                    self.default, self.datatype
+                ), f"'{self.default}' is not of type '{self.datatype}'"
+        return self
+
+    @model_validator(mode="after")
+    def check_default_values_allowed(self) -> Self:
+        """
+        Checks that the given default values
+        are in the allowed list
+        """
+        if self.default and self.allowed:
+            values = self.default
+            if not isinstance(self.default, list):
+                values = [self.default]
+            for v in values:  # type: ignore
+                assert (
+                    v in self.allowed
+                ), f"default value '{v}' is not in 'allowed' list"
+        return self
+
+    @model_validator(mode="after")
+    def check_allowed_datatype_consistency(self) -> Self:
+        """
+        Checks that allowed values are valid
+        datatypes
+        """
+        if self.allowed:
+            for v in self.allowed:
+                assert Datatypes.is_datatype(
+                    v, self.datatype
+                ), f"'{v}' is not of type '{self.datatype}'"
+        return self
+
+    @model_validator(mode="after")
+    def check_allowed_min_max(self) -> Self:
+        err = "'min/max' and 'allowed' cannot be used together"
+        if self.allowed is not None:
+            assert self.min is None and self.max is None, err
+        if self.min is not None or self.max is not None:
+            assert self.allowed is None, err
+        return self
+
+    @model_validator(mode="after")
+    def check_datatype(self) -> Self:
+        assert self.datatype in get_all_datatypes(
+            self.fqn
+        ), f"'{self.datatype}' is not a valid datatype"
+        self.datatype = resolve_datatype(self.datatype, self.fqn)
+        return self
+
+    @field_validator("unit")
+    @classmethod
+    def check_valid_unit(cls, v: str) -> str:
+        assert v in dynamic_units, f"'{v}' is not a valid unit"
+        return v
+
+    @model_validator(mode="after")
+    def check_datatype_matching_allowed_unit_datatypes(self) -> Self:
+        """
+        Checks that the datatype conforms to the allowed-datatypes
+        referenced in the unit if given
+        """
+        if self.unit:
+            assert Datatypes.get_type(
+                self.datatype
+            ), f"Cannot use 'unit' with complex datatype: '{self.datatype}'"
+            assert any(
+                Datatypes.is_subtype_of(self.datatype.rstrip("[]"), a)
+                for a in dynamic_units[self.unit]
+            ), f"'{self.datatype}' is not allowed for unit '{self.unit}'"
+        return self
+
+
+class VSSDataProperty(VSSDataDatatype):
+    pass
+
+
+class VSSDataSensor(VSSDataDatatype):
+    pass
+
+
+class VSSDataActuator(VSSDataDatatype):
+    pass
+
+
+class VSSDataStruct(VSSData):
+    pass
+
+
+class VSSDataAttribute(VSSDataDatatype):
+    pass
+
+
+TYPE_CLASS_MAP = {
+    NodeType.BRANCH: VSSDataBranch,
+    NodeType.ATTRIBUTE: VSSDataAttribute,
+    NodeType.SENSOR: VSSDataSensor,
+    NodeType.ACTUATOR: VSSDataSensor,
+    NodeType.STRUCT: VSSDataStruct,
+    NodeType.PROPERTY: VSSDataProperty,
+}
+
+
+def resolve_vss_raw(model: VSSRaw) -> VSSData:
+    """
+    Resolves a raw model to the actual node that
+    it should be validated to
+    """
+    model = VSSData(**model.model_dump(exclude_none=True))
+    cls = TYPE_CLASS_MAP.get(model.type)
+    if not cls:
+        log.warning(f"No class mapping for type='{model.type.value}'")
+        raise ModelException()
+    model = cls(**model.model_dump(exclude_none=True))
+    return model  # type: ignore
+
+
+def get_vss_raw(data: dict[str, Any], fqn: str | None) -> VSSRaw:
+    """
+    Tries to build a VSSNode and falls back to the
+    raw node
+    """
+    model = VSSRaw(fqn=fqn, **data)
+    try:
+        model = resolve_vss_raw(model)
+    except (ValidationError, ModelException):
+        log.debug(f"'{fqn}', incomplete, initialized as '{model.__class__.__name__}'")
+        return model
+    return model
+
+
+def get_all_model_fields() -> list[str]:
+    fields = set()
+    for c in TYPE_CLASS_MAP.values():
+        for k in c.model_fields.keys():  # type: ignore
+            fields.add(k)
+    log.debug(f"Core model, fields={len(fields)}")
+    return list(fields)
diff --git a/src/vss_tools/vspec/model/__init__.py b/src/vss_tools/vspec/model/__init__.py
deleted file mode 100644
index 31f95c9f..00000000
--- a/src/vss_tools/vspec/model/__init__.py
+++ /dev/null
@@ -1,7 +0,0 @@
-# Copyright (c) 2021 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
diff --git a/src/vss_tools/vspec/model/constants.py b/src/vss_tools/vspec/model/constants.py
deleted file mode 100644
index 7d251229..00000000
--- a/src/vss_tools/vspec/model/constants.py
+++ /dev/null
@@ -1,305 +0,0 @@
-# Copyright (c) 2021 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-#
-# Constant Types and Mappings
-#
-# noinspection PyPackageRequirements
-from __future__ import annotations
-from vss_tools import log
-import re
-import sys
-from collections import abc
-from collections.abc import Sequence
-from enum import Enum, EnumMeta
-from typing import TextIO, TypeVar
-
-import yaml
-
-NON_ALPHANUMERIC_WORD = re.compile("[^A-Za-z0-9]+")
-
-T = TypeVar("T")
-
-
-class VSSUnit(str):
-    """String subclass for storing unit information."""
-
-    id: str  # Typically abbreviation like "V"
-    unit: str | None = None  # Typically full name like "Volt"
-    definition: str | None = None
-    quantity: str | None = None  # Typically quantity, like "Voltage"
-    allowed_datatypes: list[str] | None = None  # Typically quantity, like "Voltage"
-
-    def __new__(
-        cls,
-        id: str,
-        unit: str | None = None,
-        definition: str | None = None,
-        quantity: str | None = None,
-        allowed_datatypes: list[str] | None = None,
-    ) -> VSSUnit:
-        self = super().__new__(cls, id)
-        self.id = id
-        self.unit = unit
-        self.definition = definition
-        self.quantity = quantity
-        self.allowed_datatypes = allowed_datatypes
-        return self
-
-    @property
-    def value(self):
-        return self
-
-
-class VSSQuantity(str):
-    """String subclass for storing quantity information."""
-
-    id: str  # Identifier preferably taken from a standard, like ISO 80000
-    definition: str  # Explanation of quantity, for example reference to standard
-    remark: str | None = None  # remark as defined in for example ISO 80000
-    comment: str | None = None
-
-    def __new__(
-        cls,
-        id: str,
-        definition: str,
-        remark: str | None = None,
-        comment: str | None = None,
-    ) -> VSSQuantity:
-        self = super().__new__(cls, id)
-        self.id = id
-        self.definition = definition
-        self.remark = remark
-        self.comment = comment
-        return self
-
-    @property
-    def value(self):
-        return self
-
-
-class EnumMetaWithReverseLookup(EnumMeta):
-    """This class extends EnumMeta and adds:
-    - from_str(str): reverse lookup
-    - values(): sequence of values
-    """
-
-    def __new__(typ, *args, **kwargs):
-        cls = super().__new__(typ, *args, **kwargs)
-        if not hasattr(cls, "__reverse_lookup__"):
-            cls.__reverse_lookup__ = {v.value: v for v in cls.__members__.values()}
-        if not hasattr(cls, "__values__"):
-            cls.__values__ = tuple(v.value for v in cls.__members__.values())
-        return cls
-
-    def from_str(cls: type[T], value: str) -> T:
-        return cls.__reverse_lookup__[value]  # type: ignore[attr-defined]
-
-    def values(cls: type[T]) -> Sequence[str]:
-        return cls.__values__  # type: ignore[attr-defined]
-
-
-class VSSType(Enum, metaclass=EnumMetaWithReverseLookup):
-    BRANCH = "branch"
-    ATTRIBUTE = "attribute"
-    SENSOR = "sensor"
-    ACTUATOR = "actuator"
-    STRUCT = "struct"
-    PROPERTY = "property"
-
-
-class VSSDataType(Enum, metaclass=EnumMetaWithReverseLookup):
-    INT8 = "int8"
-    UINT8 = "uint8"
-    INT16 = "int16"
-    UINT16 = "uint16"
-    INT32 = "int32"
-    UINT32 = "uint32"
-    INT64 = "int64"
-    UINT64 = "uint64"
-    BOOLEAN = "boolean"
-    FLOAT = "float"
-    DOUBLE = "double"
-    STRING = "string"
-    INT8_ARRAY = "int8[]"
-    UINT8_ARRAY = "uint8[]"
-    INT16_ARRAY = "int16[]"
-    UINT16_ARRAY = "uint16[]"
-    INT32_ARRAY = "int32[]"
-    UINT32_ARRAY = "uint32[]"
-    INT64_ARRAY = "int64[]"
-    UINT64_ARRAY = "uint64[]"
-    BOOLEAN_ARRAY = "boolean[]"
-    FLOAT_ARRAY = "float[]"
-    DOUBLE_ARRAY = "double[]"
-    STRING_ARRAY = "string[]"
-
-    @classmethod
-    def is_numeric(cls, datatype):
-        """
-        Return true if this datatype accepts numerical values
-        """
-        if datatype in [
-            VSSDataType.STRING,
-            VSSDataType.STRING_ARRAY,
-            VSSDataType.BOOLEAN,
-            VSSDataType.BOOLEAN_ARRAY,
-        ]:
-            return False
-        return True
-
-
-class VSSUnitCollection:
-    units: dict[str, VSSUnit] = dict()
-
-    @staticmethod
-    def get_config_dict(yaml_file: TextIO, key: str) -> dict[str, dict[str, str]]:
-        yaml_config = yaml.safe_load(yaml_file)
-        if (len(yaml_config) == 1) and (key in yaml_config):
-            # Old style unit file
-            configs = yaml_config.get(key, {})
-        else:
-            # New style unit file
-            configs = yaml_config
-        return configs
-
-    @classmethod
-    def reset_units(cls):
-        cls.units = dict()
-
-    @classmethod
-    def load_config_file(cls, config_file: str) -> int:
-        added_configs = 0
-        with open(config_file) as my_yaml_file:
-            my_units = cls.get_config_dict(my_yaml_file, "units")
-            added_configs = len(my_units)
-            for k, v in my_units.items():
-                unit = k
-                if "unit" in v:
-                    unit = v["unit"]
-                elif "label" in v:
-                    # Old syntax
-                    unit = v["label"]
-                definition = None
-                if "definition" in v:
-                    definition = v["definition"]
-                elif "description" in v:
-                    # Old syntax
-                    definition = v["description"]
-
-                quantity = None
-                if "quantity" in v:
-                    quantity = v["quantity"]
-                elif "domain" in v:
-                    # Old syntax
-                    quantity = v["domain"]
-                else:
-                    log.error("No quantity (domain) found for unit %s", k)
-                    sys.exit(-1)
-
-                if (VSSQuantityCollection.nbr_quantities() > 0) and (
-                    VSSQuantityCollection.get_quantity(quantity) is None
-                ):
-                    # Only give info on first occurance and only if quantities exist at all
-                    log.info(
-                        "Quantity %s used by unit %s has not been defined", quantity, k
-                    )
-                    VSSQuantityCollection.add_quantity(quantity)
-
-                allowed_datatypes = None
-
-                if "allowed_datatypes" in v:
-                    allowed_datatypes = []
-                    for datatype in v["allowed_datatypes"]:
-                        allowed_datatypes.append(datatype)
-                        if datatype == "numeric":
-                            # Symbolic type for all numeric types
-                            continue
-                        try:
-                            VSSDataType.from_str(datatype)
-                        except KeyError:
-                            log.error(
-                                "Unknown datatype %s in unit definition", datatype
-                            )
-                            sys.exit(-1)
-
-                unit_node = VSSUnit(k, unit, definition, quantity, allowed_datatypes)
-                if k in cls.units:
-                    log.warning("Redefinition of unit %s", k)
-                cls.units[k] = unit_node
-        return added_configs
-
-    @classmethod
-    def get_unit(cls, id: str) -> VSSUnit | None:
-        if id in cls.units:
-            return cls.units[id]
-        else:
-            return None
-
-
-class VSSQuantityCollection:
-    quantities: dict[str, VSSQuantity] = dict()
-
-    @classmethod
-    def reset_quantities(cls):
-        cls.quantities = dict()
-
-    @classmethod
-    def load_config_file(cls, config_file: str) -> int:
-        added_quantities = 0
-        with open(config_file) as my_yaml_file:
-            my_quantities = yaml.safe_load(my_yaml_file)
-            added_quantities = len(my_quantities)
-            for k, v in my_quantities.items():
-                if isinstance(v, abc.Mapping) and "definition" in v:
-                    definition = v["definition"]
-                else:
-                    log.error("No definition found for quantity %s", k)
-                    sys.exit(-1)
-                remark = None
-                if "remark" in v:
-                    remark = v["remark"]
-                comment = None
-                if "comment" in v:
-                    comment = v["comment"]
-
-                quantity_node = VSSQuantity(k, definition, remark, comment)
-                if k in cls.quantities:
-                    log.warning("Redefinition of quantity %s", k)
-                cls.quantities[k] = quantity_node
-        return added_quantities
-
-    @classmethod
-    def get_quantity(cls, id: str) -> VSSQuantity | None:
-        if id in cls.quantities:
-            return cls.quantities[id]
-        else:
-            return None
-
-    @classmethod
-    def nbr_quantities(cls) -> int:
-        return len(cls.quantities)
-
-    @classmethod
-    def add_quantity(cls, id: str) -> None:
-        if id not in cls.quantities:
-            quantity_node = VSSQuantity(id, "Automatically generated quantity")
-            cls.quantities[id] = quantity_node
-
-
-class VSSTreeType(Enum, metaclass=EnumMetaWithReverseLookup):
-    SIGNAL_TREE = "signal_tree"
-    DATA_TYPE_TREE = "data_type_tree"
-
-    def available_types(self):
-        if self.value == "signal_tree":
-            available_types = {"branch", "sensor", "actuator", "attribute"}
-        else:
-            available_types = {"branch", "struct", "property"}
-
-        return available_types
diff --git a/src/vss_tools/vspec/model/exceptions.py b/src/vss_tools/vspec/model/exceptions.py
deleted file mode 100644
index 5a559be0..00000000
--- a/src/vss_tools/vspec/model/exceptions.py
+++ /dev/null
@@ -1,25 +0,0 @@
-# Copyright (c) 2023 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-
-class NameStyleValidationException(Exception):
-    def __init__(self, message):
-        # Call the base class constructor with the parameters it needs
-        super().__init__(message)
-
-
-class ImpossibleMergeException(Exception):
-    def __init__(self, message):
-        # Call the base class constructor with the parameters it needs
-        super().__init__(message)
-
-
-class IncompleteElementException(Exception):
-    def __init__(self, message):
-        # Call the base class constructor with the parameters it needs
-        super().__init__(message)
diff --git a/src/vss_tools/vspec/model/vsstree.py b/src/vss_tools/vspec/model/vsstree.py
deleted file mode 100644
index fa346afe..00000000
--- a/src/vss_tools/vspec/model/vsstree.py
+++ /dev/null
@@ -1,663 +0,0 @@
-# Copyright (c) 2021 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-import copy
-import re
-import sys
-from typing import Any, List, Optional, Set
-from anytree import PreOrderIter  # type: ignore[import]
-from vss_tools import log
-
-
-from anytree import (  # type: ignore[import]
-    ChildResolverError,
-    Node,
-    RenderTree,
-    Resolver,
-)
-
-from vss_tools.vspec.model.constants import (
-    VSSDataType,
-    VSSType,
-    VSSUnit,
-    VSSUnitCollection,
-)
-from vss_tools.vspec.model.exceptions import (
-    ImpossibleMergeException,
-    IncompleteElementException,
-    NameStyleValidationException,
-)
-
-DEFAULT_SEPARATOR = "."
-ARRAY_SUBSCRIPT_OP = "[]"
-
-
-class VSSNode(Node):
-    """Representation of an VSS element according to the vehicle signal specification."""
-
-    type: VSSType
-    description = None
-    comment: str = ""
-    uuid: str = ""
-    # data type - string representation. For struct names, this is the fully
-    # qualified struct name.
-    data_type_str: str = ""
-    # data type - enum representation if available
-    datatype: Optional[VSSDataType]
-
-    # The node types that the nodes can take
-    available_types: Set[str] = set()
-
-    core_attributes = [
-        "type",
-        "children",
-        "datatype",
-        "description",
-        "unit",
-        "uuid",
-        "min",
-        "max",
-        "allowed",
-        "instantiate",
-        "aggregate",
-        "default",
-        "instances",
-        "deprecation",
-        "arraysize",
-        "comment",
-        "$file_name$",
-        "fka",
-        "delete",
-        "constUID",
-    ]
-
-    # List of accepted extended attributes. In strict terminate if an attribute is
-    # neither in core or extended,
-    whitelisted_extended_attributes: List[str] = []
-
-    unit: Optional[VSSUnit] = None
-
-    min: int | float | None = None
-    max: int | float | None = None
-
-    allowed = ""
-    instantiate = True
-
-    ttl_name = ""
-
-    default = ""
-
-    instances = None
-    expanded = False
-    deprecation = ""
-    fka = ""
-    delete: bool = False
-    constUID: str | None = None
-
-    # Reference nodes for using definitions from previously existing
-    reference_tree: Optional["VSSNode"] = None
-    resolver = Resolver()
-
-    @classmethod
-    def set_reference_tree(cls, tree: Optional["VSSNode"]) -> None:
-        cls.reference_tree = tree
-
-    @classmethod
-    def get_reference_datatype(cls, node: "VSSNode") -> Optional[str]:
-        search_name = node.qualified_name()
-
-        if cls.reference_tree is None:
-            return None
-
-        for vss_element in PreOrderIter(cls.reference_tree):
-            if vss_element.qualified_name() == search_name:
-                if vss_element.has_datatype():
-                    return vss_element.datatype
-
-        log.warning("No old datatype found for %s", search_name)
-        return None
-
-    @classmethod
-    def get_reference_type(cls, node: "VSSNode") -> Optional[str]:
-        search_name = node.qualified_name()
-
-        if cls.reference_tree is None:
-            return None
-
-        for vss_element in PreOrderIter(cls.reference_tree):
-            if vss_element.qualified_name() == search_name:
-                return vss_element.type
-
-        log.warning("No old type found for %s", search_name)
-        return None
-
-    def __deepcopy__(self, memo):
-        # Deep copy of source_dict and children needed as overlay or programmatic changes
-        # in exporters otherwise risk changing values not only for current instances but also for others
-        return VSSNode(
-            self.name,
-            copy.deepcopy(self.source_dict),
-            self.available_types.copy(),
-            parent=None,
-            children=copy.deepcopy(self.children, memo),
-        )
-
-    def __init__(
-        self,
-        name,
-        source_dict: dict,
-        available_types: Set[str],
-        parent=None,
-        children=None,
-        break_on_unknown_attribute=False,
-        break_on_name_style_violation=False,
-    ):
-        """Creates an VSS Node object from parsed yaml instance represented as a dict.
-
-        Args:
-            name: Name of this VSS instance.
-            source_dict: VSS instance represented as dict from yaml parsing.
-            available_types: Available node types asa string list
-            parent: Optional parent of this node instance.
-            children: Optional children instances of this node.
-            break_on_unknown_attribute: Throw if the node contains attributes not in core VSS specification
-            break_on_name_style_vioation: Throw if this node's name is not follwing th VSS recommended style
-
-        Returns:
-            VSSNode object according to the Vehicle Signal Specification.
-
-        """
-
-        super().__init__(name, parent, children)
-        self.available_types = available_types
-
-        self.source_dict = source_dict
-        self.unpack_source_dict()
-
-        if self.is_property() and not self.parent.is_struct():
-            log.error(
-                f"Orphan property detected. {self.name} is not defined under a struct"
-            )
-            sys.exit(-1)
-
-        try:
-            self.validate_name_style(self.source_dict["$file_name$"])
-        except NameStyleValidationException as e:
-            info_string = str(e)
-            if break_on_name_style_violation:
-                log.warning(info_string)
-                log.error("You asked for strict checking. Terminating.")
-                sys.exit(-1)
-            else:
-                log.info(info_string)
-
-    def unpack_source_dict(self):
-        self.extended_attributes = self.source_dict.copy()
-
-        # Clean special cases
-        if "children" in self.extended_attributes:
-            del self.extended_attributes["children"]
-        if "type" in self.extended_attributes:
-            del self.extended_attributes["type"]
-
-        def extractCoreAttribute(name: str):
-            if (
-                name != "children"
-                and name != "type"
-                and name in self.source_dict.keys()
-            ):
-                setattr(self, name, self.source_dict[name])
-                del self.extended_attributes[name]
-
-        # Is this an include element?
-        if "type" in self.source_dict:
-            if self.source_dict["type"] not in self.available_types:
-                log.error(
-                    f'Type not allowed: {self.source_dict["type"]}. Allowed types are {self.available_types}'
-                )
-                sys.exit(-1)
-            self.type = VSSType.from_str(self.source_dict["type"])
-        else:
-            ref_type = VSSNode.get_reference_type(self)
-            if ref_type is None:
-                log.error("No type specified for %s", self.qualified_name())
-                sys.exit(-1)
-            self.type = ref_type
-            self.source_dict["type"] = ref_type.value
-
-        for attribute in VSSNode.core_attributes:
-            extractCoreAttribute(attribute)
-
-        # Datatype and unit need special handling, so we do some further analysis
-
-        # Units are applicable only for primitives. Not user defined types.
-        if self.has_unit():
-            if not (self.is_signal() or self.is_property()):
-                log.error(
-                    "Item %s cannot have unit, only allowed for signal and property!",
-                    self.name,
-                )
-                sys.exit(-1)
-
-            unit = self.source_dict["unit"]
-            self.unit = VSSUnitCollection.get_unit(unit)
-            if self.unit is None:
-                log.error(
-                    f"Unknown unit '{unit}' for signal '{self.qualified_name()}'. Terminating."
-                )
-                sys.exit(-1)
-
-        # self.data_type shall only be set if base type is a primitive (VSSDataType)
-
-        if (self.is_signal() or self.is_property()) and not self.has_datatype():
-            # Get full name and do a check
-            datatype = VSSNode.get_reference_datatype(self)
-            if datatype is not None:
-                datatype = datatype.value
-            self.datatype = datatype
-            self.source_dict["datatype"] = datatype
-
-        if self.has_datatype():
-            if self.is_signal() or self.is_property():
-                self.data_type_str = self.source_dict["datatype"]
-                self.validate_and_set_datatype()
-            else:
-                log.error(
-                    "Item %s cannot have datatype, only allowed for signal and property!",
-                    self.name,
-                )
-                sys.exit(-1)
-        elif self.is_signal() or self.is_property():
-            raise IncompleteElementException(
-                f"Incomplete element {self.name} from {self.source_dict['$file_name$']}: "
-                f"Elements of type {self.type.value} need to have a datatype declared."
-            )
-
-        # Datatype check for unit performed first when we have set the right datatype
-
-        if self.has_unit():
-            if not self.has_datatype():
-                log.error(
-                    "Unit specified for item not using standard datatype: %s", self.name
-                )
-                sys.exit(-1)
-
-        if self.has_instances() and not self.is_branch():
-            log.error(
-                f"Only branches can be instantiated. {self.qualified_name()} is of type {self.type}"
-            )
-            sys.exit(-1)
-
-    def validate_name_style(self, sourcefile):
-        """Checks wether this node is adhering to VSS style conventions.
-
-        Throws NameStyleValidationException when deviations are detected. A VSS model violating
-        this conventions can still be a valid model.
-
-        """
-        camel_regexp = re.compile("[A-Z][A-Za-z0-9]*$")
-        if (
-            self.is_signal()
-            and self.datatype == VSSDataType.BOOLEAN
-            and not self.name.startswith("Is")
-        ):
-            raise NameStyleValidationException(
-                (
-                    f'Boolean node "{self.name}" found in file "{sourcefile}" is not following naming conventions. ',
-                    'It is recommended that boolean nodes start with "Is".',
-                )
-            )
-
-        camel_regexp = re.compile("[A-Z][A-Za-z0-9]*$")
-        # relax camel case requirement for struct properties
-        if not self.is_property() and not camel_regexp.match(self.name):
-            raise NameStyleValidationException(
-                (
-                    f'Node "{self.name}" found in file "{sourcefile}" is not following naming conventions. ',
-                    "It is recommended that node names use camel case, starting with a capital letter, ",
-                    "only using letters A-z and numbers 0-9.",
-                )
-            )
-
-    def base_data_type_str(self) -> str:
-        """
-        This gives the base type of the type, i.e. without array suffix if present
-        """
-        suffix = "[]"
-        if self.data_type_str.endswith(suffix):
-            return self.data_type_str[: -len(suffix)]
-        return self.data_type_str
-
-    def qualified_name(self, separator=DEFAULT_SEPARATOR) -> str:
-        """Returns fully qualified name of a VSS object (including path) using the defined separator (or default ='.')
-        Args:
-            separator: Optional parameter as custom separator between path elements of this instance
-
-        Returns:
-            Fully Qualified VSS Node string representation including complete path.
-
-        """
-
-        name = self.name
-
-        path = self
-        while not path.is_root:
-            path = path.parent
-            node_name = path.name
-
-            name = f"{node_name}{separator}{name}"
-        return name
-
-    def is_branch(self):
-        return self.type == VSSType.BRANCH
-
-    def is_sensor(self):
-        return self.type == VSSType.SENSOR
-
-    def is_actuator(self):
-        return self.type == VSSType.ACTUATOR
-
-    def is_attribute(self):
-        return self.type == VSSType.ATTRIBUTE
-
-    def is_struct(self):
-        return self.type == VSSType.STRUCT
-
-    def is_property(self):
-        return self.type == VSSType.PROPERTY
-
-    def is_signal(self):
-        return self.is_sensor() or self.is_actuator() or self.is_attribute()
-
-    def is_orphan(self) -> bool:
-        """Checks if this instance is a branch without any child nodes
-
-        Returns:
-            True if this instance is a branch and has no children.
-        """
-        if self.is_branch() or self.is_struct():
-            return self.is_leaf
-        return False
-
-    def get_struct_qualified_name(self, struct_name) -> Optional[str]:
-        """
-        Returns whether a struct node with the given relative name is defined under the branch of this node.
-        A relative name is the fully qualified name of the struct without the branch prefix under which it is defined.
-
-        Example 1:
-        struct: VehicleTypes.Branch1.StructA
-        this: VehicleTypes.Branch1.StructB.Property1 can use data type "StructA"
-
-        Example 2:
-        struct: VehicleTypes.Branch1.StructA
-        this: VehicleTypes.Branch1.Branch2.StructB.Property1 CANNOT use data type "StructA" since they are not defined
-        under the same branch
-
-
-        Keyword arguments:
-        struct_name - The struct name to search for.
-
-        Returns:
-        Fully qualified name of the struct if one exists. Otherwise None.
-        """
-        path = self
-
-        # find the ancestor branch
-        root = None
-        while not path.is_branch():
-            path = path.parent
-            root = path
-
-        # find the struct node under the branch
-        if root is None:
-            return None
-
-        for child in root.children:
-            if child.is_struct() and child.name == struct_name:
-                return child.qualified_name()
-
-        return None
-
-    def is_instantiated(self) -> bool:
-        """Checks if node shall be instantiated through its parent
-
-        Returns:
-            True if it shall be instantiated
-        """
-        return self.instantiate
-
-    def has_unit(self) -> bool:
-        """Checks if this instance has a unit
-
-        Returns:
-            True if this instance has a unit, False otherwise
-        """
-        return hasattr(self, "unit") and self.unit is not None
-
-    def get_unit(self) -> str:
-        """Returns:
-        The name of the unit or empty string if no unit
-        """
-        if hasattr(self, "unit") and self.unit is not None:
-            return self.unit.value
-        else:
-            return ""
-
-    def has_datatype(self) -> bool:
-        """Check if this instance has a datatype
-
-        Returns:
-            True if this instance has a data type, False otherwise
-        """
-        return hasattr(self, "datatype") and self.datatype is not None
-
-    def get_datatype(self) -> str:
-        """Returns:
-        The name of the datatype or empty string if no datatype
-        """
-        return self.data_type_str
-
-    def has_instances(self) -> bool:
-        """Check if this instance has a VSS instances
-
-        Returns:
-            True if this instance declares instances, False otherwise
-        """
-        return hasattr(self, "instances") and self.instances is not None
-
-    def merge(self, other: "VSSNode"):
-        """Merges two VSSNode, other parameter overwrites the caller object,
-        if it is not None
-         Args:
-             other: other node to merge into the caller object
-        """
-        if self.is_branch() and not other.is_branch():
-            raise ImpossibleMergeException(
-                (
-                    f"Impossible merging {self.name} from {self.source_dict['$file_name$']} with {other.name} ",
-                    f"from {other.source_dict['$file_name$']}, can not change branch to {other.type.value}.",
-                )
-            )
-        elif not self.is_branch() and other.is_branch():
-            raise ImpossibleMergeException(
-                f"Impossible merging {self.name} from {self.source_dict['$file_name$']} with {other.name} "
-                f"from {other.source_dict['$file_name$']}, can not change {self.type.value} to branch."
-            )
-
-        self.source_dict.update(other.source_dict)
-        self.unpack_source_dict()
-
-    def validate_and_set_datatype(self):
-        """
-        For signals:
-        Validate that the data type string represents the corresponding VSSDataType enumeration
-
-        For properties:
-        Validate that
-        1. the data type string represents the corresponding VSSDataType enumeration, OR
-        2. the data type string refers to a struct name that is defined under the same branch.
-        Note that only struct names relative to the branch under which they are defined are checked.
-        Data types provided as fully qualified struct names are skipped from validation as they
-        require the entire tree to be rendered first. These are validated after the entire tree is rendered.
-        """
-        is_array = ARRAY_SUBSCRIPT_OP in self.data_type_str
-        # get the base name without subscript decoration
-        undecorated_datatype_str = self.data_type_str.split(DEFAULT_SEPARATOR)[
-            -1
-        ].replace(ARRAY_SUBSCRIPT_OP, "")
-
-        try:
-            self.datatype = VSSDataType.from_str(self.data_type_str)
-
-            if self.unit and self.unit.allowed_datatypes:
-                if not (
-                    (undecorated_datatype_str in self.unit.allowed_datatypes)
-                    or (
-                        VSSDataType.is_numeric(self.datatype)
-                        and "numeric" in self.unit.allowed_datatypes
-                    )
-                ):
-                    log.error(
-                        "Datatype %s not allowed for unit %s",
-                        self.data_type_str,
-                        self.unit.id,
-                    )
-                    sys.exit(-1)
-        except KeyError as e:
-            if self.type == VSSType.PROPERTY:
-                # Fully Qualified name as data type name
-                if DEFAULT_SEPARATOR in self.data_type_str:
-                    log.info(
-                        (
-                            f"Qualified datatype name {self.data_type_str} provided in node {self.qualified_name()}. ",
-                            "Semantic checks will be performed after the entire tree is rendered. SKIPPING NOW...",
-                        )
-                    )
-                else:
-                    # Custom data types can contain names defined under the
-                    # same branch
-                    struct_fqn = self.get_struct_qualified_name(
-                        undecorated_datatype_str
-                    )
-                    if struct_fqn is None:
-                        log.error(
-                            f"Data type not found. Data Type: {undecorated_datatype_str}"
-                        )
-                        sys.exit(-1)
-
-                    # replace data type with qualified name
-                    if is_array:
-                        self.data_type_str = struct_fqn + ARRAY_SUBSCRIPT_OP
-                    else:
-                        self.data_type_str = struct_fqn
-            elif self.is_signal():
-                # This is a signal possibly referencing a user-defined type.
-                # Just assign the string value for now. Validation will be
-                # performed after the entire tree is rendered.
-                log.debug(
-                    f"Possible struct-type encountered - {self.data_type_str} in node {self.name}. "
-                )
-            else:
-                raise e
-            self.datatype = None  # reset the enum
-
-    def does_attribute_exist(
-        self, other: "VSSNode", attr_fn, other_attr_fn, other_filter_fn
-    ):
-        """
-        Returns whether the an attribute of this node exists as another attribute in the specified tree
-
-        Keyword arguments:
-        other: Tree root of the search tree
-        attr_fn: Attribute projection function for this node that takes in a VSSNode as argument.
-        other_attr_fn: Attribute projection function for nodes in the search tree. The argument is of type VSSNode.
-        other_filter_fn: A filter function for node search in the "other" tree. The argument is of type VSSNode.
-        """
-        key = attr_fn(self)
-        return key in set(VSSNode.get_tree_attrs(other, other_attr_fn, other_filter_fn))
-
-    @staticmethod
-    def node_exists(root, node_name) -> bool:
-        """Checks if a node with the name provided to this method exists
-        Args:
-            root: root node of tree or root of search if search is applied to subtree
-            node_name: name of the node that is searched for. Full path (excluding root) is required.
-        """
-        try:
-            r = Resolver()
-            r.get(root, node_name)
-            return True
-        except ChildResolverError:
-            return False
-
-    def verify_attributes(self, abort_on_unknown_attribute: bool):
-        """
-        Validates a VSS object. Checks if it has the minimum parameters (description, type, uuid) and if the optional
-        parameters are supported within the specification.
-        Should not be used directly on overlays as the requirement to have all present is not relevant for
-        overlays, but rather for the final result after emrging.
-        """
-
-        # Type presence should have been tested earlier, but is tested here again for completeness
-        if "type" not in self.source_dict.keys():
-            log.error("Invalid VSS element %s, must have type", self.name)
-            sys.exit(-1)
-
-        if "description" not in self.source_dict.keys():
-            log.error("Invalid VSS element %s, must have description", self.name)
-            sys.exit(-1)
-
-        unknown = []
-        for aKey in self.source_dict.keys():
-            if (
-                aKey not in VSSNode.core_attributes
-                and aKey not in VSSNode.whitelisted_extended_attributes
-            ):
-                unknown.append(aKey)
-
-        unknown_found = False
-        if len(unknown) > 0:
-            log.warning(
-                f"Attribute(s) {', '.join(map(str, unknown))} in element {self.name} not a core "
-                "or known extended attribute."
-            )
-            unknown_found = True
-
-        if "default" in self.source_dict.keys():
-            if self.source_dict["type"] not in {
-                "attribute",
-                "property",
-                "sensor",
-                "actuator",
-            }:
-                log.warning(
-                    "Invalid VSS element %s, %s cannot use default",
-                    self.name,
-                    self.source_dict["type"],
-                )
-                unknown_found = True
-
-        if unknown_found and abort_on_unknown_attribute:
-            log.error("You asked for strict checking. Terminating.")
-            sys.exit(-1)
-
-    @staticmethod
-    def get_tree_attrs(node: "VSSNode", proj_fn, filter_fn) -> List[Any]:
-        """
-        Collect all attributes of tree nodes rooted at `node` by applying the specified projection and filter function.
-
-        Keyword arguments:
-        node: The tree root node
-        proj_fn: A function that takes in the tree node as input and returns a node attribute (projection)
-        filter_fn: A function that takes in the tree node as input and
-                   returns True if the node should be processed in the result set/False otherwise.
-
-        Returns:
-        Projection of nodes that meet the filter criterion specified.
-        """
-        return [proj_fn(n) for _, _, n in RenderTree(node) if filter_fn(n)]
diff --git a/src/vss_tools/vspec/tree.py b/src/vss_tools/vspec/tree.py
new file mode 100644
index 00000000..e35ed68f
--- /dev/null
+++ b/src/vss_tools/vspec/tree.py
@@ -0,0 +1,563 @@
+# Copyright (c) 2023 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+from __future__ import annotations
+import re
+from typing import Any
+import uuid
+
+from anytree import Node, PreOrderIter, find, findall
+from copy import deepcopy
+
+from vss_tools import log
+from vss_tools.vspec.vspec import deep_update
+from vss_tools.vspec.model import (
+    VSSData,
+    VSSDataDatatype,
+    VSSDataStruct,
+    VSSRaw,
+    get_vss_raw,
+    VSSDataBranch,
+    resolve_vss_raw,
+    ModelValidationException,
+)
+from pydantic import ValidationError
+from vss_tools.vspec.datatypes import Datatypes, dynamic_datatypes
+
+SEPARATOR = "."
+
+
+class NotMergeableException(Exception):
+    pass
+
+
+class NoRootsException(Exception):
+    pass
+
+
+class MultipleRootsException(Exception):
+    pass
+
+
+class InvalidExpansionEntryException(Exception):
+    pass
+
+
+class NoVSSDataException(Exception):
+    pass
+
+
+class VSSNode(Node):  # type: ignore[misc]
+    """
+    Our Anytree node class
+    It contains the actual node data in the `data` attribute
+    """
+
+    separator = SEPARATOR
+
+    def __init__(
+        self, name: str, fqn: str | None, data: dict[str, Any], **kwargs: Any
+    ) -> None:
+        super().__init__(name, **kwargs)
+        self.data = get_vss_raw(data, fqn)
+        self.uuid: str | None = None
+
+    def copy(self) -> VSSNode:
+        node = VSSNode(
+            self.name,
+            self.get_fqn(),
+            self.data.model_dump(
+                mode="json",
+                exclude=set(["fqn"]),
+            ),
+        )
+        for c in self.children:
+            cp = c.copy()
+            cp.parent = node
+
+        return node
+
+    def _post_attach(self, parent: VSSNode):
+        """
+        Updating the data fqn when getting reattached.
+        We need the fqn in the data for validation purposes.
+        """
+        log.debug(
+            f"Got attached to parent='{parent.get_fqn()}', new fqn='{self.get_fqn()}'"
+        )
+        self.data.fqn = self.get_fqn(SEPARATOR)
+
+    def _post_detach(self, parent: VSSNode):
+        log.debug(f"'{self.get_fqn()}', detached from parent='{parent.get_fqn()}'")
+
+    def get_vss_data(self) -> VSSData:
+        if not isinstance(self.data, VSSData):
+            raise NoVSSDataException(
+                f"'{self.get_fqn()}' data does not contain 'VSSData'"
+            )
+        else:
+            return self.data
+
+    def get_fqn(self, sep: str = SEPARATOR) -> str:
+        return sep.join([n.name for n in self.path])
+
+    def resolve(self) -> None:
+        """
+        Resolves raw nodes into "higher" nodes
+        """
+        vss_raw_nodes = findall(
+            self, filter_=lambda node: isinstance(node.data, VSSRaw)
+        )
+        for node in vss_raw_nodes:
+            try:
+                node.data = resolve_vss_raw(node.data)
+            except ValidationError as e:
+                raise ModelValidationException(node.get_fqn(), e)
+
+    def get_child(self, fqn: str) -> VSSNode | None:
+        for child in self.children:
+            if child.get_fqn() == fqn:
+                log.debug(f"{self.get_fqn()}, found child='{child.get_fqn()}'")
+                return child
+        return None
+
+    def merge(self, other: VSSNode) -> None:
+        """
+        Merges this node with another one.
+        The data of the other node has priority.
+        Also merges children if their fqn matches recursively
+        """
+        if self.get_fqn() != other.get_fqn():
+            raise NotMergeableException(f"{self.get_fqn()} != {other.get_fqn()}")
+
+        log.debug(f"{self.get_fqn()}, merging with='{other.get_fqn()}'")
+        self_data = self.data.as_dict(exclude_fields=["fqn"])
+        other_data = other.data.as_dict(exclude_fields=["fqn"])
+
+        deep_update(self_data, other_data)
+        self.data = get_vss_raw(self_data, self.get_fqn())
+
+        child: VSSNode
+        for child in other.children:
+            match = self.get_child(child.get_fqn())
+            if match:
+                match.merge(child)
+            else:
+                child.parent = self
+
+    def add_uuids(self) -> None:
+        VSS_NAMESPACE = "vehicle_signal_specification"
+        namespace_uuid = uuid.uuid5(uuid.NAMESPACE_OID, VSS_NAMESPACE)
+        node: VSSNode
+        for node in PreOrderIter(self):
+            node.uuid = uuid.uuid5(namespace_uuid, node.get_fqn()).hex
+
+    def get_instance_nodes(self) -> tuple[VSSNode, ...]:
+        return findall(
+            self,
+            filter_=lambda node: isinstance(node.data, VSSDataBranch)
+            and node.data.instances,
+        )
+
+    def get_node_with_fqn(self, fqn: str, sep: str = SEPARATOR) -> VSSNode | None:
+        result = find(self, filter_=lambda n: n.get_fqn(sep) == fqn)
+        if result:
+            return result
+        return None
+
+    def connect(self, fqn: str, node: VSSNode) -> VSSNode | None:
+        """
+        Connects a node with a given fqn to this one
+        It automatically generates missing branches in between.
+        However, we are not generating valid branch entries
+        (description is missing). This is on purpose so that
+        the tree in the end fails if those values are not getting
+        filled
+        """
+        connected = self.get_node_with_fqn(fqn)
+        if connected:
+            log.debug(f"Already connected: {fqn}")
+            return connected
+        target_fqn = get_expected_parent(fqn)
+        if not target_fqn:
+            return None
+        target = self.get_node_with_fqn(target_fqn)
+        if target:
+            node.parent = target
+            return target
+        else:
+            auto_node = VSSNode(
+                get_name(target_fqn),
+                target_fqn,
+                {"type": "branch"},
+            )
+            node.parent = auto_node
+            return self.connect(target_fqn, auto_node)
+
+    def expand_instances(self) -> None:
+        """
+        Expanding all nodes that have configured
+        "instances".
+        It will generate new nodes below this one
+        """
+        instance_nodes = self.get_instance_nodes()
+        instance_node: VSSNode
+        iterations = 0
+        n_instance_nodes = 0
+        # We need to setup a loop here since it could be that
+        # we add nodes that again have instances configured
+        while instance_nodes:
+            n_instance_nodes += len(instance_nodes)
+            iterations += 1
+            for instance_node in instance_nodes:
+                log.debug(f"'{instance_node.get_fqn()}', expanding...")
+                # Copy the reference node for creating instances
+                instance_node_copy = deepcopy(instance_node)
+
+                # PERF: instance_node_copy = instance_node.copy()
+                # does not work here because it does not include copying
+                # parents and therefore children fqns are different
+                # However we are using the fqns to decide where to insert them
+                # Fixing that would increase performance a bit since `deepcopy`
+                # is very slow
+
+                # Remove children from copy that should not be instantiatet
+                for child in instance_node_copy.children:
+                    if not getattr(child.data, "instantiate", True):
+                        log.debug(
+                            f"'{child.get_fqn()}', removing from copy (instantiate=False)"
+                        )
+                        child.parent = None
+
+                # Remove children from instance node that need to be put on created instances instead
+                for child in instance_node.children:
+                    if getattr(child.data, "instantiate", True):
+                        log.debug(
+                            f"'{child.get_fqn()}', removing from node (instantiate=True)"
+                        )
+                        child.parent = None
+
+                # Roots to attach generated nodes
+                # Initialized with the instance node itself
+                roots = [instance_node]
+                log.debug(f"Roots: {[r.get_fqn() for r in roots]}")
+
+                # We want to keep track of generated nodes
+                # in order to append common original children
+                # in the end
+                generated_instance_nodes = []
+                # On every iteration, we get back new nodes to attach new instances to (roots)
+                # as well as the nodes that have been generated
+                for instance in instance_node.data.instances:  # type: ignore
+                    roots, generated = expand_instance(
+                        roots, instance_node_copy, instance
+                    )
+                    generated_instance_nodes.extend(generated)
+
+                # Since we do not want to append original children
+                # to intermediate generated nodes, we filter for the leafs
+                generated_instance_leaf_nodes = []
+                for node in generated_instance_nodes:
+                    if node.is_leaf:
+                        generated_instance_leaf_nodes.append(node)
+
+                # Appending all original children at the right place
+                # Possibly overwriting content if wished
+                add_expanded_instance_children(
+                    generated_instance_leaf_nodes, instance_node, instance_node_copy
+                )
+
+                instance_node.data.instances = []  # type: ignore
+            instance_nodes = self.get_instance_nodes()
+        if iterations:
+            log.debug(f"Instances, iterations={iterations}, nodes={n_instance_nodes}")
+
+    def delete_nodes(self, nodes: tuple[VSSNode]) -> None:
+        """
+        Deleting given nodes.
+        It is not checked whether nodes are reachable from self!
+        """
+        size_before = self.size
+        for node in nodes:
+            log.debug(f"Deleting node: {node}")
+            node.parent = None
+        size_after = self.size
+        if nodes:
+            log.info(
+                f"Nodes deleted, given={len(nodes)}, overall={size_before - size_after}"
+            )
+
+    def get_naming_violations(self) -> list[list[str]]:
+        """
+        Gets a list of nodes that are violating the naming conventions
+        It returns a list of fqn's and their violation reason
+        """
+        violations = []
+        log.debug(f"Checking node name compliance for {self.name}")
+        camel_case_pattern = re.compile("[A-Z][A-Za-z0-9]*$")
+        for node in PreOrderIter(self):
+            match = re.match(camel_case_pattern, node.name)
+            if not match:
+                violations.append([node.get_fqn(), "not CamelCase"])
+            if isinstance(node.data, VSSDataDatatype):
+                if node.data.datatype == Datatypes.BOOLEAN[0]:
+                    if not node.name.startswith("Is") and not node.name.startswith("Has"):
+                        violations.append([node.get_fqn(), "Not starting with 'Is' or 'Has'"])
+        if violations:
+            log.info(f"Naming violations: {len(violations)}")
+        return violations
+
+    def get_extra_attributes(self, allowed: tuple[str, ...]) -> list[list[str]]:
+        """
+        Gets a list of attributes that are not in the vss model
+        and not explicitly allowed in the given list
+        """
+        violations = []
+        for node in PreOrderIter(self):
+            for field in node.data.get_extra_attributes():
+                if field not in allowed:
+                    violations.append([node.get_fqn(), field])
+        if violations:
+            log.warning(f"Attributes, violations={len(violations)}")
+        return violations
+
+    def as_flat_dict(self, with_extra_attributes: bool) -> dict[str, Any]:
+        """
+        Generates a flat dict and whether to include
+        user attributes or not
+        """
+        data = {}
+        node: VSSNode
+        for node in PreOrderIter(self):
+            key = node.get_fqn()
+            data[key] = node.data.as_dict(with_extra_attributes)
+            if node.uuid:
+                data[key]["uuid"] = node.uuid
+        return data
+
+
+def get_expected_parent(name: str) -> str | None:
+    """
+    Returns the parent of a given fqn
+    E.g. "A.B.C" -> "A.B"
+    """
+    parent = SEPARATOR.join(name.split(SEPARATOR)[:-1])
+    if parent == name:
+        return None
+    if parent == "":
+        return None
+    return parent
+
+
+def get_name(key: str) -> str:
+    return key.split(SEPARATOR)[-1]
+
+
+def find_children_ids(node_ids: list[str], name: str) -> list[str]:
+    """
+    Gets all direnct children of a given name.
+    E.g. "A.B" -> ["A.B.C", "A.B.D"]
+    """
+    ids = []
+    for i in node_ids:
+        if get_expected_parent(i) == name:
+            ids.append(i)
+    return ids
+
+
+def add_expanded_instance_children(
+    roots: list[VSSNode], instance_root: VSSNode, instance_copy: VSSNode
+):
+    """
+    Adds initial children of node that started the instance expansion (instance_root)
+    The initial state of the node has been freezed in instance_copy.
+    The current points in the tree where to attach children is in roots
+    """
+
+    # We want to find nodes to add to the roots.
+    # Nodes that area already in the instance_root
+    # Should not be readded to the roots but should be replaced
+    add = []
+    child: VSSNode
+    for child in instance_copy.children:
+        match = instance_root.get_child(child.get_fqn())
+        if not match:
+            add.append(child)
+
+    log.debug(f"Add to instances: {[n.get_fqn() for n in add]}")
+
+    # Adding them..
+    for root in roots:
+        for a in add:
+            c = a.copy()
+            c.parent = root
+
+    # Now searching for all initial children
+    # that are in the current tree
+    # Those are the ones we need to update
+    change = []
+    for child in instance_copy.children:
+        match = instance_root.get_child(child.get_fqn())
+        if match:
+            change.append([match, child])
+
+    log.debug(f"Change nodes: {[n[0].get_fqn() for n in change]}")
+
+    for nodes in change:
+        target = nodes[0]
+        src = nodes[1]
+        target.merge(src)
+        # We found a place for the node to be changed
+        # Exclude it from future processing
+        src.parent = None
+
+
+def expand_instance(
+    roots: list[VSSNode],
+    template: VSSNode,
+    instance: list[str] | str,
+) -> tuple[list[VSSNode], list[VSSNode]]:
+    """
+    Expands a given instance.
+    roots represents the current points where to attach
+    new nodes to. Depending on how many new nodes
+    will be generated or how the instance was provided,
+    we are returning the initial roots or new roots with the
+    template contains a node copy for generating nodes
+    generated nodes.
+    """
+    # The behavior is different depending how instances have been defined
+    # Instances could be again a list of strings
+    # We want to harmonize that
+    # The info however is used to decide what new root points to return
+    requested_instances = []
+    if isinstance(instance, list):
+        requested_instances = instance
+    else:
+        requested_instances = [instance]
+
+    names = []
+    # Node names can be given with a range syntax
+    # such as Foo[1,2]asdf
+    # We expand them here
+    log.debug(f"Requested instances: {requested_instances}")
+    for i in requested_instances:
+        names.extend(expand_string(str(i)))
+    nodes = []
+    log.debug(f"Requested instance names: {names}")
+    for name in names:
+        for root in roots:
+            # Need to copy the template so that we
+            # are not messing with the same node
+            node = template.copy()
+            node.name = name
+            node.data.instances = []  # type: ignore
+            node.parent = root
+            nodes.append(node)
+            node.children = []
+    # New roots to attach to get returned
+    # either when using the Row[1,2] syntax
+    # or when specifying instances as a list entry
+    # Otherwise next instance generation call will
+    # attach nodes to the same roots again
+    if len(names) > 1 or isinstance(instance, list):
+        return nodes, nodes
+    else:
+        return roots, nodes
+
+
+def count_seperator(s: str) -> int:
+    return s.count(SEPARATOR)
+
+
+def build_tree(
+    data: dict[str, Any], connect_orphans: bool = False
+) -> tuple[VSSNode, dict[str, VSSNode]]:
+    """
+    Building a tree out of raw dictionary data.
+    Also tries to find orphans and connects orphans
+    if desired.
+    """
+    nodes: dict[str, VSSNode] = {}
+
+    for k in sorted(data.keys(), key=count_seperator):
+        v: Any = data.get(k)
+        parent = get_expected_parent(k)
+        node_name = get_name(k)
+        node = VSSNode(node_name, k, v)
+        # If this is a datatype tree we should add the
+        # struct node as a datatype
+        if isinstance(node.data, VSSDataStruct):
+            dynamic_datatypes.add(k)
+
+        # We connect a parent and children
+        # Probably we should be fine only connecting
+        # children since we are sorting for separators
+        node.children = []
+        if parent and parent in nodes:
+            node.parent = nodes[parent]
+        else:
+            for child_id in find_children_ids(list(nodes.keys()), k):
+                node.children.append(nodes[child_id])
+        nodes[k] = node
+
+    roots = []
+    orphans = {}
+    # Finding roots and orphans based on presence of a separator
+    for fqn, node in nodes.items():
+        if not node.parent:
+            if SEPARATOR not in fqn:
+                roots.append(node)
+            else:
+                orphans[fqn] = node
+
+    if not roots:
+        raise NoRootsException()
+
+    if len(roots) > 1:
+        raise MultipleRootsException(f"{[r.name for r in roots]}")
+
+    root: VSSNode = roots[0]
+    if connect_orphans:
+        connected_fqns = []
+        for fqn, orphan in orphans.items():
+            connected = root.connect(fqn, orphan)
+            if connected:
+                connected_fqns.append(fqn)
+        for fqn in connected_fqns:
+            del orphans[fqn]
+
+    if orphans:
+        log.warning(f"Orphans: {len(orphans)}")
+
+    log.debug(f"Tree, root='{root.name}', size={root.size}, height={root.height}")
+    return root, orphans
+
+
+def expand_string(s: str) -> list[str]:
+    """
+    Expands or unrolls a string with a range syntax
+    definitions inside.
+
+    Example: "abc[1,4]bar"
+    Result:
+    - abc1bar
+    - abc2bar
+    - abc3bar
+    - abc4bar
+
+    """
+    pattern = r".*(\[(\d+),(\d+)\]).*"
+    match = re.match(pattern, s)
+    if not match:
+        return [s]
+    expanded = []
+    if int(match.group(2)) > int(match.group(3)):
+        raise InvalidExpansionEntryException(f"Invalid range: '{match.group(1)}'")
+    for i in range(int(match.group(2)), int(match.group(3)) + 1):
+        expanded.append(s.replace(match.group(1), str(i)))
+    return expanded
diff --git a/src/vss_tools/vspec/units_quantities.py b/src/vss_tools/vspec/units_quantities.py
new file mode 100644
index 00000000..a2230b93
--- /dev/null
+++ b/src/vss_tools/vspec/units_quantities.py
@@ -0,0 +1,54 @@
+# Copyright (c) 2023 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+from pathlib import Path
+from vss_tools.vspec.model import VSSUnit, VSSQuantity, ModelValidationException
+from pydantic import ValidationError
+import yaml
+from vss_tools import log
+
+
+class UnitQuantityRedefinitionException(Exception):
+    pass
+
+
+class MalformedDictException(Exception):
+    pass
+
+
+def load_units_or_quantities(
+    files: list[Path], class_type: type[VSSUnit | VSSQuantity]
+) -> dict[str, VSSUnit | VSSQuantity]:
+    data = {}
+    for file in files:
+        content = yaml.safe_load(file.read_text())
+        if not content:
+            log.warning(f"{file}, empty")
+            continue
+        log.info(
+            f"Loaded '{class_type.__name__}', file={file.absolute()}, elements={len(content)}"
+        )
+        for k, v in content.items():
+            if v is None:
+                log.error(f"'{class_type.__name__}', '{k}' is 'None'")
+                raise MalformedDictException()
+            if k in data:
+                log.error(f"'{class_type.__name__}', redefinition of '{k}'")
+                raise UnitQuantityRedefinitionException()
+            try:
+                data[k] = class_type(**v)
+            except ValidationError as e:
+                raise ModelValidationException(k, e)
+    return data
+
+
+def load_units(unit_files: list[Path]) -> dict[str, VSSUnit]:
+    return load_units_or_quantities(unit_files, VSSUnit)  # type: ignore[return-value]
+
+
+def load_quantities(quantities: list[Path]) -> dict[str, VSSQuantity]:
+    return load_units_or_quantities(quantities, VSSQuantity)  # type: ignore[return-value]
diff --git a/src/vss_tools/vspec/utils/__init__.py b/src/vss_tools/vspec/utils/__init__.py
index bb881195..d670520d 100644
--- a/src/vss_tools/vspec/utils/__init__.py
+++ b/src/vss_tools/vspec/utils/__init__.py
@@ -6,18 +6,3 @@
 # https://www.mozilla.org/en-US/MPL/2.0/
 #
 # SPDX-License-Identifier: MPL-2.0
-
-import argparse
-
-
-def remove_options_argparse(parser: argparse.ArgumentParser, options) -> None:
-    # ToDo: this allows removing arguments from argparse but involves accessing protected members
-    #  also when removing '--format' it removes the positional argument vspec_file
-    for option in options:
-        for action in parser._actions:
-            if (
-                vars(action)["option_strings"]
-                and vars(action)["option_strings"][0] == option
-            ):
-                parser._handle_conflict_resolve(action, [(option, action)])
-                break
diff --git a/src/vss_tools/vspec/utils/stringstyle.py b/src/vss_tools/vspec/utils/misc.py
similarity index 70%
rename from src/vss_tools/vspec/utils/stringstyle.py
rename to src/vss_tools/vspec/utils/misc.py
index 53243c62..a6cba7d5 100644
--- a/src/vss_tools/vspec/utils/stringstyle.py
+++ b/src/vss_tools/vspec/utils/misc.py
@@ -5,10 +5,20 @@
 # https://www.mozilla.org/en-US/MPL/2.0/
 #
 # SPDX-License-Identifier: MPL-2.0
-
+from typing import Any
 import re
 
 
+def getattr_nn(o: object, name: str, default: Any | None = None) -> Any:
+    """
+    Wraps getattr() but will also use 'default' if result is None
+    """
+    result = getattr(o, name, default)
+    if result is None and default is not None:
+        result = default
+    return result
+
+
 def camel_case(st):
     """Camel case string conversion"""
     s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", st)
diff --git a/src/vss_tools/vspec/utils/vss2id_val.py b/src/vss_tools/vspec/utils/vss2id_val.py
index 38d0e5b6..273cfafb 100644
--- a/src/vss_tools/vspec/utils/vss2id_val.py
+++ b/src/vss_tools/vspec/utils/vss2id_val.py
@@ -9,10 +9,10 @@
 from vss_tools import log
 import sys
 from typing import Optional
+from anytree import PreOrderIter
 
-from anytree import PreOrderIter  # type: ignore
 
-from vss_tools.vspec.model.vsstree import VSSNode
+from vss_tools.vspec.tree import VSSNode
 from vss_tools.vspec.utils.idgen_utils import fnv1_32_wrapper
 
 
@@ -28,14 +28,15 @@ def validate_static_uids(signals_dict: dict, validation_tree: VSSNode, strict: b
     def check_description(k: str, v: dict, match_tuple: tuple):
         validation_node: VSSNode = validation_tree_nodes[match_tuple[1]]
 
+        data = validation_node.get_vss_data()
         try:
-            assert v["description"] == validation_node.description
+            assert v["description"] == data.description
 
         except AssertionError:
             log.warning(
                 "[Validation] "
                 f"DESCRIPTION MISMATCH: The description of {k} has changed from "
-                f"\n\t   Validation: '{validation_node.description}' to \n\t   Current "
+                f"\n\t   Validation: '{data.description}' to \n\t   Current "
                 f"vspec: '{v['description']}'"
             )
 
@@ -54,13 +55,10 @@ def check_semantics(k: str, v: dict, strict_mode: bool) -> Optional[int]:
             for fka_val in v["fka"]:
                 old_static_uid = "0x" + fnv1_32_wrapper(fka_val, v, strict_mode)
                 for i, validation_node in enumerate(validation_tree_nodes):
-                    if (
-                        old_static_uid
-                        == validation_node.extended_attributes["staticUID"]
-                    ):
+                    if old_static_uid == validation_node.data.staticUID:
                         log.warning(
                             f"[Validation] SEMANTIC NAME CHANGE or PATH CHANGE for '{k}', "
-                            f"it used to be '{validation_node.qualified_name()}'."
+                            f"it used to be '{validation_node.get_fqn()}'."
                         )
                         semantic_match = i
             return semantic_match
@@ -70,13 +68,16 @@ def check_semantics(k: str, v: dict, strict_mode: bool) -> Optional[int]:
     def check_deprecation(k: str, v: dict, match_tuple: tuple):
         if (
             "deprecation" in v.keys()
-            and validation_tree_nodes[match_tuple[1]].deprecation
+            and validation_tree_nodes[match_tuple[1]].data.deprecation
         ):
-            if v["deprecation"] != validation_tree_nodes[match_tuple[1]].deprecation:
+            if (
+                v["deprecation"]
+                != validation_tree_nodes[match_tuple[1]].data.deprecation
+            ):
                 log.warning(
                     f"[Validation] DEPRECATION MSG CHANGE: Deprecation message "
                     f"for '{k}' was "
-                    f"'{validation_tree_nodes[match_tuple[1]].deprecation}' "
+                    f"'{validation_tree_nodes[match_tuple[1]].data.deprecation}' "
                     f"in validation but now is '{v['deprecation']}'."
                 )
 
@@ -100,17 +101,18 @@ def hashed_pipeline():
         for key, value in signals_dict.items():
             matched_uids = []
             for id_validation_tree, other_node in enumerate(validation_tree_nodes):
-                if value["staticUID"] == other_node.extended_attributes["staticUID"]:
-                    if key != other_node.qualified_name():
+                other_sUID = getattr(other_node.data, "staticUID", None)
+                if value["staticUID"] == other_sUID:
+                    if key != other_node.get_fqn():
                         _ = check_semantics(key, value, strict)
                     matched_uids.append((key, id_validation_tree))
             # if not matched via UID check semantics or path change
             if len(matched_uids) == 0:
                 semantic_match = check_semantics(key, value, strict)
                 if semantic_match is None:
-                    key_found: bool = False
+                    key_found = False
                     for i, node in enumerate(validation_tree_nodes):
-                        if key == node.qualified_name():
+                        if key == node.get_fqn():
                             key_found = True
                             validation_tree_nodes.pop(i)
                             break
@@ -147,7 +149,7 @@ def hashed_pipeline():
         for node in validation_tree_nodes:
             log.warning(
                 "[Validation] DELETED ATTRIBUTE: "
-                f"'{node.qualified_name()}' was not matched so it must have "
+                f"'{node.get_fqn()}' was not matched so it must have "
                 f"been deleted."
             )
 
diff --git a/src/vss_tools/vspec/vspec.py b/src/vss_tools/vspec/vspec.py
index 96a66d6a..02808f67 100644
--- a/src/vss_tools/vspec/vspec.py
+++ b/src/vss_tools/vspec/vspec.py
@@ -5,52 +5,137 @@
 # https://www.mozilla.org/en-US/MPL/2.0/
 #
 # SPDX-License-Identifier: MPL-2.0
+from __future__ import annotations
 
-import logging
-import rich_click as click
-from vss_tools.vspec.lazy_group import LazyGroup
-import vss_tools.vspec.cli_options as clo
-from vss_tools import log
 from pathlib import Path
+from typing import Any
+
+import yaml
+
+from vss_tools import log
+
+
+class IncludeStatementException(Exception):
+    pass
+
+
+class IncludeNotFoundException(Exception):
+    pass
 
 
-@click.group(context_settings={"auto_envvar_prefix": "vss_tools"}, invoke_without_command=True)
-@clo.log_level_opt
-@clo.log_file_opt
-@click.version_option()
-@click.pass_context
-def cli(ctx: click.Context, log_level: str, log_file: Path):
-    if ctx.invoked_subcommand is None:
-        click.echo(ctx.get_help())
-    if log_file:
-        file_handler = logging.FileHandler(log_file, mode="w")
-        file_handler.setFormatter(
-            logging.Formatter("%(asctime)s:%(levelname)s:%(message)s")
+class InvalidSpecDuplicatedEntryException(Exception):
+    pass
+
+
+class SpecException(Exception):
+    pass
+
+
+class Include:
+    def __init__(self, statement: str, prefix: str | None = None):
+        self.statement = statement
+        split = statement.split()
+        if len(split) < 2:
+            raise IncludeStatementException(f"Malformed include statement: {statement}")
+        self.target = split[1]
+        self.prefix = prefix
+        if len(split) == 3:
+            if self.prefix is not None:
+                self.prefix += f".{split[2]}"
+            else:
+                self.prefix = split[2]
+
+    def resolve_path(self, include_dirs: list[Path]) -> Path:
+        for dir in include_dirs:
+            path = dir / self.target
+            if path.exists():
+                log.debug(f"'{self.statement}', resolved={path}")
+                return path
+        raise IncludeNotFoundException(
+            f"Unable to find include {self.target}. Include dirs: {include_dirs}"
         )
-        log.addHandler(file_handler)
-
-    log.setLevel(log_level)
-
-
-@cli.group(
-    cls=LazyGroup,
-    lazy_subcommands={
-        "binary": "vss_tools.vspec.vssexporters.vss2binary:cli",
-        "csv": "vss_tools.vspec.vssexporters.vss2csv:cli",
-        "ddsidl": "vss_tools.vspec.vssexporters.vss2ddsidl:cli",
-        "franca": "vss_tools.vspec.vssexporters.vss2franca:cli",
-        "graphql": "vss_tools.vspec.vssexporters.vss2graphql:cli",
-        "id": "vss_tools.vspec.vssexporters.vss2id:cli",
-        "json": "vss_tools.vspec.vssexporters.vss2json:cli",
-        "jsonschema": "vss_tools.vspec.vssexporters.vss2jsonschema:cli",
-        "protobuf": "vss_tools.vspec.vssexporters.vss2protobuf:cli",
-        "yaml": "vss_tools.vspec.vssexporters.vss2yaml:cli",
-    },
-)
-@click.pass_context
-def export(ctx: click.Context):
-    """
-    Export a vspec to a chosen format
-    """
-    if ctx.invoked_subcommand is None:
-        click.echo(ctx.get_help())
+
+
+def deep_update(base: dict[str, Any], update: dict[str, Any]) -> None:
+    for key, value in update.items():
+        if isinstance(value, dict):
+            if key in base and isinstance(base[key], dict):
+                deep_update(base[key], value)
+            else:
+                base[key] = value
+        else:
+            base[key] = value
+
+
+class VSpec:
+    def __init__(
+        self,
+        source: Path,
+        prefix: str | None = None,
+    ):
+        self.source = source
+        self.prefix = prefix
+
+        content = source.read_text()
+
+        self.data = yaml.safe_load(content)
+        if self.data is None:
+            self.data = {}
+
+        if prefix:
+            tmp_data = {}
+            for k, v in self.data.items():
+                new_key = f"{prefix}.{k}"
+                tmp_data[new_key] = v
+            self.data = tmp_data
+
+        lines = content.splitlines()
+        include_statements = [
+            line.strip() for line in lines if line.strip().startswith("#include")
+        ]
+        self.includes = [Include(statement, prefix) for statement in include_statements]
+
+    def __str__(self) -> str:
+        return f"{self.__class__.__name__}, src={self.source}, prefix={self.prefix}, includes={len(self.includes)}"
+
+    def update(self, other: VSpec) -> None:
+        deep_update(self.data, other.data)
+
+
+def get_vspecs(
+    includes: list[Path], spec: Path, prefix: str | None = None
+) -> list[VSpec]:
+    vspecs: list[VSpec] = []
+    vspec = VSpec(spec, prefix)
+    vspecs.append(vspec)
+
+    for include in vspec.includes:
+        include_spec = include.resolve_path(includes + [vspec.source.parent])
+        vspecs.extend(get_vspecs(includes, include_spec, include.prefix))
+
+    return vspecs
+
+
+def load_vspec(
+    include_dirs: list[Path], specs: list[Path], identifier: str | None = None
+) -> VSpec:
+    spec = None
+    vspecs: list[VSpec] = []
+    for s in specs:
+        includes = [s.parent] + include_dirs
+        vspecs.extend(get_vspecs(includes, s))
+    pre = "VSpecs"
+    if identifier:
+        pre += f" ({identifier})"
+    log.info(f"{pre} loaded, amount={len(vspecs)}")
+    for vspec in vspecs:
+        log.debug(vspec)
+        if spec is None:
+            spec = vspec
+        else:
+            spec.update(vspec)
+    if not spec:
+        msg = f"Weird behavior. Could not load any spec: {specs}"
+        log.error(msg)
+        raise SpecException(msg)
+    return spec
diff --git a/src/vss_tools/vspec/vssexporters/utils.py b/src/vss_tools/vspec/vssexporters/utils.py
deleted file mode 100644
index 2f93a21e..00000000
--- a/src/vss_tools/vspec/vssexporters/utils.py
+++ /dev/null
@@ -1,155 +0,0 @@
-# Copyright (c) 2023 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-from vss_tools import log
-from vss_tools.vspec import (
-    load_quantities,
-    load_units,
-    verify_mandatory_attributes,
-    load_tree,
-    merge_tree,
-    check_type_usage,
-    expand_tree_instances,
-    clean_metadata,
-)
-from vss_tools.vspec import VSSNode, VSSTreeType, VSpecError
-from pathlib import Path
-import sys
-
-
-class ArgumentException(Exception):
-    pass
-
-
-def get_trees(
-    include_dirs: tuple[Path],
-    aborts: tuple[str],
-    strict: bool,
-    extended_attributes: tuple[str],
-    uuid: bool,
-    quantities: tuple[Path],
-    vspec: Path,
-    units: tuple[Path],
-    types: tuple[Path, ...],
-    types_output: Path | None,
-    overlays: tuple[Path],
-    expand: bool,
-) -> tuple[VSSNode, VSSNode | None]:
-    includes = list(include_dirs)
-    includes.extend([Path.cwd(), vspec.parent])
-
-    abort_on_unknown_attribute = False
-    abort_on_namestyle = False
-
-    if "unknown-attributes" in aborts or strict:
-        abort_on_unknown_attribute = True
-    if "name-style" in aborts or strict:
-        abort_on_namestyle = True
-
-    if extended_attributes:
-        VSSNode.whitelisted_extended_attributes = list(extended_attributes)
-        log.info(f"Known extended attributes: {', '.join(extended_attributes)}")
-    else:
-        extended_attributes = list()
-
-    # Follow up to https://github.com/COVESA/vehicle_signal_specification/pull/721
-    # Deprecate --uuid
-    if uuid:
-        log.warning(
-            "The argument --uuid is deprecated and the uuid feature is planned"
-            "to be removed in VSS-tools 6.0"
-        )
-        log.info("If you need static identifiers consider using the vspec2id tool")
-
-    qs = [str(q) for q in quantities]
-    load_quantities(str(vspec), qs)
-    us = [str(u) for u in units]
-    load_units(str(vspec), us)
-
-    # process data type tree
-    data_type_tree = None
-    if types_output:
-        if types_output and not types:
-            raise ArgumentException(
-                "An output file for data types was provided. Please also provide "
-                "the input vspec file for data types"
-            )
-    if types:
-        data_type_tree = processDataTypeTree(
-            types_output, types, includes, abort_on_namestyle
-        )
-        verify_mandatory_attributes(data_type_tree, abort_on_unknown_attribute)
-
-    try:
-        log.info(f"Loading vspec from {vspec}...")
-        tree = load_tree(
-            str(vspec),
-            includes,
-            VSSTreeType.SIGNAL_TREE,
-            break_on_name_style_violation=abort_on_namestyle,
-            expand_inst=False,
-            data_type_tree=data_type_tree,
-        )
-
-        VSSNode.set_reference_tree(tree)
-
-        for overlay in overlays:
-            log.info(f"Applying VSS overlay from {overlay}...")
-            othertree = load_tree(
-                str(overlay),
-                includes,
-                VSSTreeType.SIGNAL_TREE,
-                break_on_name_style_violation=abort_on_namestyle,
-                expand_inst=False,
-                data_type_tree=data_type_tree,
-            )
-            merge_tree(tree, othertree)
-
-        check_type_usage(tree, VSSTreeType.SIGNAL_TREE, data_type_tree)
-        if expand:
-            expand_tree_instances(tree)
-
-        clean_metadata(tree)
-        verify_mandatory_attributes(tree, abort_on_unknown_attribute)
-        return tree, data_type_tree
-
-    except VSpecError as e:
-        log.error(f"Error: {e}")
-        sys.exit(255)
-
-
-def processDataTypeTree(
-    types_output: Path | None,
-    types: tuple[Path, ...],
-    include_dir: list[Path],
-    abort_on_namestyle: bool,
-) -> VSSNode:
-    """
-    Helper function to process command line arguments and invoke logic for processing data
-    type information provided in vspec format
-    """
-    if types_output:
-        log.info("Sensors and custom data types will be consolidated into one file.")
-
-    first_tree = True
-    for type_file in types:
-        log.info(f"Loading and processing struct/data type tree from {type_file}")
-        new_tree = load_tree(
-            str(type_file),
-            include_dir,
-            VSSTreeType.DATA_TYPE_TREE,
-            break_on_name_style_violation=abort_on_namestyle,
-            expand_inst=False,
-        )
-        if first_tree:
-            tree = new_tree
-            first_tree = False
-        else:
-            merge_tree(tree, new_tree)
-    check_type_usage(tree, VSSTreeType.DATA_TYPE_TREE)
-    return tree
diff --git a/src/vss_tools/vspec/vssexporters/vss2binary.py b/src/vss_tools/vspec/vssexporters/vss2binary.py
index 2289a8b4..16926475 100644
--- a/src/vss_tools/vspec/vssexporters/vss2binary.py
+++ b/src/vss_tools/vspec/vssexporters/vss2binary.py
@@ -13,58 +13,20 @@
 from vss_tools import log
 import vss_tools.vspec.cli_options as clo
 import rich_click as click
-from vss_tools.vspec.model.vsstree import VSSType
-from vss_tools.vspec.vssexporters.utils import get_trees
-
-out_file = ""
-_cbinary = None
-
-
-def createBinaryCnode(
-    fname,
-    nodename,
-    nodetype,
-    uuid,
-    description,
-    nodedatatype,
-    nodemin,
-    nodemax,
-    unit,
-    allowed,
-    defaultAllowed,
-    validate,
-    children,
-):
-    global _cbinary
-    _cbinary.createBinaryCnode(
-        fname,
-        nodename,
-        nodetype,
-        uuid,
-        description,
-        nodedatatype,
-        nodemin,
-        nodemax,
-        unit,
-        allowed,
-        defaultAllowed,
-        validate,
-        children,
-    )
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.main import get_trees
 
 
 def allowedString(allowedList):
     allowedStr = ""
     for elem in allowedList:
         allowedStr += hexAllowedLen(elem) + elem
-    #    print("allowedstr=" + allowedStr + "\n")
     return allowedStr
 
 
 def hexAllowedLen(allowed):
     hexDigit1 = len(allowed) // 16
     hexDigit2 = len(allowed) - hexDigit1 * 16
-    #    print("Hexdigs:" + str(hexDigit1) + str(hexDigit2))
     return "".join([intToHexChar(hexDigit1), intToHexChar(hexDigit2)])
 
 
@@ -75,87 +37,28 @@ def intToHexChar(hexInt):
         return chr(hexInt - 10 + ord("A"))
 
 
-def export_node(node, generate_uuid, out_file):
-    nodename = str(node.name)
-    b_nodename = nodename.encode("utf-8")
-
-    nodetype = str(node.type.value)
-    b_nodetype = nodetype.encode("utf-8")
-
-    nodedescription = str(node.description)
-    b_nodedescription = nodedescription.encode("utf-8")
-
-    children = len(node.children)
-
-    nodedatatype = ""
-    nodemin = ""
-    nodemax = ""
-    nodeunit = ""
-    nodeallowed = ""
-    nodedefault = ""
-    nodeuuid = ""
-    nodevalidate = ""  # exported to binary
-
-    if (
-        node.type == VSSType.SENSOR
-        or node.type == VSSType.ACTUATOR
-        or node.type == VSSType.ATTRIBUTE
-    ):
-        nodedatatype = str(node.datatype.value)
-    b_nodedatatype = nodedatatype.encode("utf-8")
-
-    # many optional attributes are initilized to "" in vsstree.py
-    if node.min is not None:
-        nodemin = str(node.min)
-    b_nodemin = nodemin.encode("utf-8")
-
-    if node.max is not None:
-        nodemax = str(node.max)
-    b_nodemax = nodemax.encode("utf-8")
-
-    if node.allowed != "":
-        nodeallowed = allowedString(node.allowed)
-    b_nodeallowed = nodeallowed.encode("utf-8")
-
-    if node.default != "":
-        nodedefault = str(node.default)
-    b_nodedefault = nodedefault.encode("utf-8")
-
-    # in case of unit or aggregate, the attribute will be missing
-    try:
-        nodeunit = str(node.unit.value)
-    except AttributeError:
-        pass
-    b_nodeunit = nodeunit.encode("utf-8")
-
-    if generate_uuid:
-        nodeuuid = node.uuid
-    b_nodeuuid = nodeuuid.encode("utf-8")
-
-    if "validate" in node.extended_attributes:
-        nodevalidate = node.extended_attributes["validate"]
-    b_nodevalidate = nodevalidate.encode("utf-8")
-
-    b_fname = out_file.encode("utf-8")
-
-    createBinaryCnode(
-        b_fname,
-        b_nodename,
-        b_nodetype,
-        b_nodeuuid,
-        b_nodedescription,
-        b_nodedatatype,
-        b_nodemin,
-        b_nodemax,
-        b_nodeunit,
-        b_nodeallowed,
-        b_nodedefault,
-        b_nodevalidate,
-        children,
+def export_node(cdll: ctypes.CDLL, node: VSSNode, generate_uuid, out_file: str):
+    uuid = "" if node.uuid is None else node.uuid
+    data = node.get_vss_data().as_dict()
+    cdll.createBinaryCnode(
+        out_file.encode(),
+        node.name.encode(),
+        data.get("type", "").encode(),
+        uuid.encode(),
+        data.get("description", "").encode(),
+        data.get("datatype", "").encode(),
+        str(data.get("min", "")).encode(),
+        str(data.get("max", "")).encode(),
+        data.get("unit", "").encode(),
+        b""
+        if data.get("allowed") is None
+        else allowedString(data.get("allowed", "")).encode(),
+        str(data.get("default", "")).encode(),
+        str(data.get("validate", "")).encode(),
+        len(node.children),
     )
-
     for child in node.children:
-        export_node(child, generate_uuid, out_file)
+        export_node(cdll, child, generate_uuid, out_file)
 
 
 @click.command()
@@ -191,10 +94,8 @@ def cli(
     """
     Export to Binary.
     """
-    global _cbinary
-    _cbinary = ctypes.CDLL(str(bintool_dll))
-
-    _cbinary.createBinaryCnode.argtypes = (
+    cdll = ctypes.CDLL(str(bintool_dll))
+    cdll.createBinaryCnode.argtypes = (
         ctypes.c_char_p,
         ctypes.c_char_p,
         ctypes.c_char_p,
@@ -209,21 +110,17 @@ def cli(
         ctypes.c_char_p,
         ctypes.c_int,
     )
-
     tree, _ = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        tuple(),
-        None,
-        overlays,
-        True,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        overlays=overlays,
     )
     log.info("Generating binary output...")
-    export_node(tree, uuid, str(output))
+    export_node(cdll, tree, uuid, str(output))
     log.info(f"Binary output generated in {output}")
diff --git a/src/vss_tools/vspec/vssexporters/vss2csv.py b/src/vss_tools/vspec/vssexporters/vss2csv.py
index 6788a87f..879a67de 100644
--- a/src/vss_tools/vspec/vssexporters/vss2csv.py
+++ b/src/vss_tools/vspec/vssexporters/vss2csv.py
@@ -8,22 +8,22 @@
 
 # Convert vspec tree to CSV
 
-
+import csv
 from pathlib import Path
 from vss_tools import log
 import rich_click as click
 import vss_tools.vspec.cli_options as clo
-from vss_tools.vspec.model.vsstree import VSSNode
-from vss_tools.vspec.vssexporters.utils import get_trees
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.utils.misc import getattr_nn
+from vss_tools.vspec.main import get_trees
 from anytree import PreOrderIter  # type: ignore[import]
-from typing import AnyStr
-
-
-# Write the header line
+from typing import Any
 
 
-def print_csv_header(file, uuid, entry_type: AnyStr, include_instance_column: bool):
-    arg_list = [
+def get_header(
+    with_uuid: bool, entry_type: str, with_instance_column: bool
+) -> list[str]:
+    row = [
         entry_type,
         "Type",
         "DataType",
@@ -36,53 +36,43 @@ def print_csv_header(file, uuid, entry_type: AnyStr, include_instance_column: bo
         "Allowed",
         "Default",
     ]
-    if uuid:
-        arg_list.append("Id")
-    if include_instance_column:
-        arg_list.append("Instances")
-    file.write(format_csv_line(arg_list))
-
-
-# Format a data or header line according to the CSV standard (IETF RFC 4180)
-
-
-def format_csv_line(csv_fields):
-    formatted_csv_line = ""
-    for csv_field in csv_fields:
-        if csv_field is None:
-            csv_field = ""
-        formatted_csv_line = (
-            formatted_csv_line + '"' + str(csv_field).replace('"', '""') + '",'
-        )
-    return formatted_csv_line[:-1] + "\n"
-
-
-# Write the data lines
+    if with_uuid:
+        row.append("Id")
+    if with_instance_column:
+        row.append("Instances")
+    return row
+
+
+def add_rows(
+    rows: list[list[Any]], root: VSSNode, with_uuid: bool, with_instance_column: bool
+) -> None:
+    node: VSSNode
+    for node in PreOrderIter(root):
+        data = node.get_vss_data()
+        row = [
+            node.get_fqn(),
+            data.type.value,
+            getattr_nn(data, "datatype", ""),
+            getattr_nn(data, "deprecation", ""),
+            getattr_nn(data, "unit", ""),
+            getattr_nn(data, "min", ""),
+            getattr_nn(data, "max", ""),
+            data.description,
+            getattr_nn(data, "comment", ""),
+            getattr_nn(data, "allowed", ""),
+            getattr_nn(data, "default", ""),
+        ]
+        if with_uuid:
+            row.append(getattr_nn(node, "uuid", ""))
+        if with_instance_column:
+            row.append(getattr_nn(data, "instances", ""))
+        rows.append(row)
 
 
-def print_csv_content(file, tree: VSSNode, uuid, include_instance_column: bool):
-    tree_node: VSSNode
-    for tree_node in PreOrderIter(tree):
-        data_type_str = tree_node.get_datatype()
-        unit_str = tree_node.get_unit()
-        arg_list = [
-            tree_node.qualified_name("."),
-            tree_node.type.value,
-            data_type_str,
-            tree_node.deprecation,
-            unit_str,
-            tree_node.min,
-            tree_node.max,
-            tree_node.description,
-            tree_node.comment,
-            tree_node.allowed,
-            tree_node.default,
-        ]
-        if uuid:
-            arg_list.append(tree_node.uuid)
-        if include_instance_column and tree_node.instances is not None:
-            arg_list.append(tree_node.instances)
-        file.write(format_csv_line(arg_list))
+def write_csv(rows: list[list[Any]], output: Path):
+    with open(output, "w", newline="") as f:
+        writer = csv.writer(f)
+        writer.writerows(rows)
 
 
 @click.command()
@@ -118,46 +108,31 @@ def cli(
     Export as CSV.
     """
     tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        types,
-        types_output,
-        overlays,
-        expand,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
+        expand=expand,
     )
     log.info("Generating CSV output...")
 
     generic_entry = datatype_tree and not types_output
-    include_instance_column = not expand
-    with open(output, "w") as f:
-        signal_entry_type = "Node" if generic_entry else "Signal"
-        print_csv_header(
-            f,
-            uuid,
-            signal_entry_type,
-            include_instance_column,
-        )
-        print_csv_content(f, tree, uuid, include_instance_column)
-        if datatype_tree is not None and generic_entry is True:
-            print_csv_content(
-                f,
-                datatype_tree,
-                uuid,
-                include_instance_column,
-            )
-
-    if datatype_tree is not None and generic_entry is False:
-        with open(types_output, "w") as f:
-            print_csv_header(f, uuid, "Node", include_instance_column)
-            print_csv_content(
-                f,
-                datatype_tree,
-                uuid,
-                include_instance_column,
-            )
+    with_instance_column = not expand
+
+    entry_type = "Node" if generic_entry else "Signal"
+    rows = [get_header(uuid, entry_type, with_instance_column)]
+    add_rows(rows, tree, uuid, with_instance_column)
+    if generic_entry and datatype_tree:
+        add_rows(rows, datatype_tree, uuid, with_instance_column)
+    write_csv(rows, output)
+
+    if not generic_entry and datatype_tree:
+        rows = [get_header(uuid, "Node", with_instance_column)]
+        add_rows(rows, datatype_tree, uuid, with_instance_column)
+        write_csv(rows, types_output)
diff --git a/src/vss_tools/vspec/vssexporters/vss2ddsidl.py b/src/vss_tools/vspec/vssexporters/vss2ddsidl.py
index 797ae337..f3b25f0f 100644
--- a/src/vss_tools/vspec/vssexporters/vss2ddsidl.py
+++ b/src/vss_tools/vspec/vssexporters/vss2ddsidl.py
@@ -14,11 +14,12 @@
 from vss_tools import log
 import rich_click as click
 import vss_tools.vspec.cli_options as clo
-from vss_tools.vspec.vssexporters.utils import get_trees
+from vss_tools.vspec.model import VSSDataBranch, VSSDataStruct
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.main import get_trees
+from vss_tools.vspec.utils.misc import getattr_nn
 from pathlib import Path
 
-from vss_tools.vspec.model.vsstree import VSSType
-
 c_keywords = [
     "auto",
     "break",
@@ -170,7 +171,7 @@ def get_allowed_enum_literal(name: str):
     return name
 
 
-idlFileBuffer = []
+idl_file_buffer = []
 
 dataTypesMap_covesa_dds = {
     "uint8": "octet",
@@ -188,102 +189,91 @@ def get_allowed_enum_literal(name: str):
 }
 
 
-def export_node(node, generate_uuid, generate_all_idl_features):
+def export_node(
+    node: VSSNode, generate_uuid: bool, generate_all_idl_features: bool
+) -> None:
     """
     This method is used to traverse VSS node and to create corresponding DDS IDL buffer string
     """
-    global idlFileBuffer
-    datatype = None
-    unit = None
-    min = None
-    max = None
-    defaultValue = None
-    allowedValues = None
-    arraysize = None
-
-    if node.type == VSSType.BRANCH:
-        idlFileBuffer.append("module " + getAllowedName(node.name))
-        idlFileBuffer.append("{")
+    global idl_file_buffer
+    arraysize = getattr(node.data, "arraysize", None)
+    allowed_values = None
+
+    if isinstance(node.data, VSSDataBranch):
+        idl_file_buffer.append("module " + getAllowedName(node.name))
+        idl_file_buffer.append("{")
         for child in node.children:
             export_node(child, generate_uuid, generate_all_idl_features)
-        idlFileBuffer.append("};")
-        idlFileBuffer.append("")
+        idl_file_buffer.append("};")
+        idl_file_buffer.append("")
     else:
-        isEnumCreated = False
+        enum_created = False
         # check if there is a need to create enum (based on the usage of allowed values)
-        if node.allowed != "":
+        allowed = getattr(node.data, "allowed")
+        datatype = getattr(node.data, "datatype")
+        if allowed:
             """
             enum should be enclosed under module block to avoid namespec conflict
             module name for enum is chosen as the node name +
             """
-            if node.datatype.value in ["string", "string[]"]:
-                idlFileBuffer.append("module " + getAllowedName(node.name) + "_M")
-                idlFileBuffer.append("{")
-                idlFileBuffer.append(
+            if datatype in ["string", "string[]"]:
+                idl_file_buffer.append("module " + getAllowedName(node.name) + "_M")
+                idl_file_buffer.append("{")
+                idl_file_buffer.append(
                     "enum "
                     + getAllowedName(node.name)
                     + "Values{"
-                    + str(
-                        ",".join(
-                            get_allowed_enum_literal(item) for item in node.allowed
-                        )
-                    )
+                    + str(",".join(get_allowed_enum_literal(item) for item in allowed))
                     + "};"
                 )
-                isEnumCreated = True
-                idlFileBuffer.append("};")
-                allowedValues = str(node.allowed)
+                enum_created = True
+                idl_file_buffer.append("};")
+                allowed_values = str(allowed)
             else:
                 print(
                     f"Warning: VSS2IDL can only handle allowed values for string type, "
-                    f"signal {node.name} has type {node.datatype.value}"
+                    f"signal {node.name} has type {datatype}"
                 )
 
-        idlFileBuffer.append("struct " + getAllowedName(node.name))
-        idlFileBuffer.append("{")
+        idl_file_buffer.append("struct " + getAllowedName(node.name))
+        idl_file_buffer.append("{")
         if generate_uuid:
-            idlFileBuffer.append("string uuid;")
+            idl_file_buffer.append("string uuid;")
         # fetching value of datatype and obtaining the equivalent DDS type
         try:
-            datatype_str = node.get_datatype()
-            if node.has_datatype():
-                if datatype_str in dataTypesMap_covesa_dds:
-                    datatype = str(dataTypesMap_covesa_dds[datatype_str])
-                elif "[" in datatype_str:
-                    nodevalueArray = datatype_str.split("[", 1)
+            if datatype:
+                if datatype in dataTypesMap_covesa_dds:
+                    datatype = str(dataTypesMap_covesa_dds[datatype])
+                elif "[" in datatype:
+                    nodevalueArray = datatype.split("[", 1)
                     if str(nodevalueArray[0]) in dataTypesMap_covesa_dds:
                         datatype = str(dataTypesMap_covesa_dds[str(nodevalueArray[0])])
                         arraysize = "[" + str(arraysize) + nodevalueArray[1]
-            else:  # no primitive type. this is custom
-                datatype = datatype_str.replace(".", "::")  # custom data type
+                else:  # no primitive type. this is custom
+                    datatype = datatype.replace(".", "::")  # custom data type
 
         except AttributeError:
             pass
         # fetching value of unit
-        try:
-            unit = str(node.unit.value)
-        except AttributeError:
-            pass
+        unit = getattr(node.data, "unit")
+        min = getattr(node.data, "min")
+        max = getattr(node.data, "max")
 
-        if node.min is not None:
-            min = str(node.min)
-        if node.max is not None:
-            max = str(node.max)
-        if node.default != "":
-            defaultValue = node.default
-            if isinstance(defaultValue, str) and not isEnumCreated:
-                defaultValue = '"' + defaultValue + '"'
+        default = getattr(node.data, "default")
+        if default:
+            if isinstance(default, str) and not enum_created:
+                default = '"' + default + '"'
 
         if datatype is not None:
             # adding range if min and max are specified in vspec file
             if min is not None and max is not None and generate_all_idl_features:
-                idlFileBuffer.append(
+                idl_file_buffer.append(
                     "@range(min=" + str(min) + " ,max=" + str(max) + ")"
                 )
 
-            if allowedValues is None:
-                if defaultValue is None:
-                    idlFileBuffer.append(
+            if allowed_values is None:
+                if default is None:
+                    idl_file_buffer.append(
                         (
                             "sequence<" + datatype + "> value"
                             if arraysize is not None
@@ -294,14 +284,14 @@ def export_node(node, generate_uuid, generate_all_idl_features):
                 else:
                     # default values in IDL file are not accepted by CycloneDDS/FastDDS :
                     # these values can be generated if --all-idl-features is set as True
-                    idlFileBuffer.append(
+                    idl_file_buffer.append(
                         (
                             "sequence<" + datatype + "> value"
                             if arraysize is not None
                             else datatype + " value"
                         )
                         + (
-                            "  default " + str(defaultValue)
+                            "  default " + str(default)
                             if generate_all_idl_features
                             else ""
                         )
@@ -309,8 +299,8 @@ def export_node(node, generate_uuid, generate_all_idl_features):
                     )
             else:
                 # this is the case where allowed values are provided, accordingly contents are converted to enum
-                if defaultValue is None:
-                    idlFileBuffer.append(
+                if default is None:
+                    idl_file_buffer.append(
                         getAllowedName(node.name)
                         + "_M::"
                         + getAllowedName(node.name)
@@ -319,37 +309,38 @@ def export_node(node, generate_uuid, generate_all_idl_features):
                 else:
                     # default values in IDL file are not accepted by CycloneDDS/FastDDS :
                     # these values can be generated if --all-idl-features is set as True
-                    idlFileBuffer.append(
+                    idl_file_buffer.append(
                         getAllowedName(node.name)
                         + "_M::"
                         + getAllowedName(node.name)
                         + "Values value"
-                        + (" " + str(defaultValue) if generate_all_idl_features else "")
+                        + (" " + str(default) if generate_all_idl_features else "")
                         + ";"
                     )
 
         if unit is not None:
-            idlFileBuffer.append(
+            idl_file_buffer.append(
                 ("" if generate_all_idl_features else "//")
                 + 'const string unit="'
                 + unit
                 + '";'
             )
 
-        idlFileBuffer.append(
+        data = node.get_vss_data()
+        idl_file_buffer.append(
             ("" if generate_all_idl_features else "//")
             + 'const string type ="'
-            + str(node.type.value)
+            + str(data.type.value)
             + '";'
         )
 
-        idlFileBuffer.append(
+        idl_file_buffer.append(
             ("" if generate_all_idl_features else "//")
             + 'const string description="'
-            + node.description
+            + data.description
             + '";'
         )
-        idlFileBuffer.append("};")
+        idl_file_buffer.append("};")
 
 
 class StructExporter(object):
@@ -367,26 +358,26 @@ def export(self, root) -> str:
         self.export_data_type_node(root)
         return self.str_buf
 
-    def export_data_type_node(self, node):
+    def export_data_type_node(self, node: VSSNode):
         """
         This method is used to traverse VSS node and to create corresponding DDS IDL buffer string
         """
 
         prefix = ""
         suffix = ""
-        if node.is_branch():
+        if isinstance(node.data, VSSDataBranch):
             prefix = f"module {getAllowedName(node.name)}" + " {\n"
             suffix = "};\n"
-        elif node.is_struct():
+        elif isinstance(node.data, VSSDataStruct):
             # check if the properties use structs that have not been seen before
             # if not, add a forward declaration
             fwds = []
             for c in node.children:
-                # primtive type
-                if c.has_datatype():
+                datatype = getattr_nn(c.data, "datatype", "")
+                if not datatype.startswith("Types."):
                     continue
 
-                datatype_str = c.get_datatype().replace(".", "::").split("[", 1)[0]
+                datatype_str = datatype(".", "::").split("[", 1)[0]
                 if datatype_str not in self.structs_seen:
                     base_type = datatype_str.split("::")[-1]
                     fwds.append(base_type)
@@ -397,10 +388,11 @@ def export_data_type_node(self, node):
 
             prefix += f"struct {getAllowedName(node.name)}" + " {\n"
             suffix = "};\n"
-            self.structs_seen.append(node.qualified_name("::").split("[", 1)[0])
+            self.structs_seen.append(node.get_fqn("::").split("[", 1)[0])
         else:
-            datatype_str = node.get_datatype().replace(".", "::").split("[", 1)[0]
-            is_seq = "[" in node.get_datatype()
+            datatype = getattr_nn(node.data, "datatype", "")
+            datatype_str = datatype.replace(".", "::").split("[", 1)[0]
+            is_seq = "[" in datatype
             if is_seq:
                 self.str_buf += (
                     f"sequence<{datatype_str}> {getAllowedName(node.name)};\n"
@@ -420,7 +412,7 @@ def export_idl(file, root, generate_uuids=True, generate_all_idl_features=False)
     -> DDS IDL equivalent string buffer and to serialize it acccordingly into a file
     """
     export_node(root, generate_uuids, generate_all_idl_features)
-    file.write("\n".join(idlFileBuffer))
+    file.write("\n".join(idl_file_buffer))
     log.info("IDL file generated at location : " + file.name)
 
 
@@ -459,18 +451,16 @@ def cli(
     Export as DDSIDL.
     """
     tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        types,
-        None,
-        overlays,
-        True,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
     )
     log.info("Generating DDS-IDL output...")
 
diff --git a/src/vss_tools/vspec/vssexporters/vss2franca.py b/src/vss_tools/vspec/vssexporters/vss2franca.py
index b6f0df02..e92c7eb4 100644
--- a/src/vss_tools/vspec/vssexporters/vss2franca.py
+++ b/src/vss_tools/vspec/vssexporters/vss2franca.py
@@ -9,15 +9,17 @@
 # Convert vspec tree to franca
 
 
+from io import TextIOWrapper
 import rich_click as click
 from pathlib import Path
-from vss_tools.vspec.vssexporters.utils import get_trees
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.main import get_trees
 import vss_tools.vspec.cli_options as clo
-from anytree import PreOrderIter  # type: ignore[import]
-
-# Write the header line
+from vss_tools import log
+from anytree import PreOrderIter
 
 
+# Write the header line
 def print_franca_header(file, version="unknown"):
     file.write(f"""
 // Copyright (C) 2022, COVESA
@@ -44,30 +46,36 @@ def print_franca_header(file, version="unknown"):
 
 
 # Write the data lines
-def print_franca_content(file, tree, uuid):
+def print_franca_content(file: TextIOWrapper, root: VSSNode, with_uuid: bool) -> None:
     output = ""
-    for tree_node in PreOrderIter(tree):
-        if tree_node.parent:
+    node: VSSNode
+    for node in PreOrderIter(root):
+        data = node.get_vss_data()
+        if node.parent:
             if output:
                 output += ",\n{"
             else:
                 output += "{"
-            output += f"\tname: \"{tree_node.qualified_name('.')}\""
-            output += f',\n\ttype: "{tree_node.type.value}"'
-            output += f',\n\tdescription: "{tree_node.description}"'
-            if tree_node.has_datatype():
-                output += f',\n\tdatatype: "{tree_node.get_datatype()}"'
-            if uuid:
-                output += f',\n\tuuid: "{tree_node.uuid}"'
-            if tree_node.has_unit():
-                output += f',\n\tunit: "{tree_node.get_unit()}"'
-            if tree_node.min is not None:
-                output += f",\n\tmin: {tree_node.min}"
-            if tree_node.max is not None:
-                output += f",\n\tmax: {tree_node.max}"
-            if tree_node.allowed:
-                output += f",\n\tallowed: {tree_node.allowed}"
-
+            output += f'\tname: "{node.get_fqn()}"'
+            output += f',\n\ttype: "{data.type.value}"'
+            output += f',\n\tdescription: "{data.description}"'
+            datatype = getattr(data, "datatype", None)
+            if datatype:
+                output += f',\n\tdatatype: "{datatype}"'
+            if with_uuid:
+                output += f',\n\tuuid: "{node.uuid}"'
+            unit = getattr(data, "unit", None)
+            if unit:
+                output += f',\n\tunit: "{unit}"'
+            min = getattr(data, "min", None)
+            if min is not None:
+                output += f",\n\tmin: {min}"
+            max = getattr(data, "max", None)
+            if max is not None:
+                output += f",\n\tmax: {max}"
+            allowed = getattr(data, "allowed", None)
+            if allowed:
+                output += f",\n\tallowed: {allowed}"
             output += "\n}"
     file.write(output)
 
@@ -100,23 +108,19 @@ def cli(
     """
     Export as Franca.
     """
-    print("Generating Franca output...")
+    log.info("Generating Franca output...")
     tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        tuple(),
-        None,
-        overlays,
-        True,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        overlays=overlays,
     )
-    outfile = open(output, "w")
-    print_franca_header(outfile, franca_vss_version)
-    print_franca_content(outfile, tree, uuid)
-    outfile.write("\n]")
-    outfile.close()
+    with open(output, "w") as f:
+        print_franca_header(f, franca_vss_version)
+        print_franca_content(f, tree, uuid)
+        f.write("\n]")
diff --git a/src/vss_tools/vspec/vssexporters/vss2graphql.py b/src/vss_tools/vspec/vssexporters/vss2graphql.py
index 5b398b18..be6452ec 100644
--- a/src/vss_tools/vspec/vssexporters/vss2graphql.py
+++ b/src/vss_tools/vspec/vssexporters/vss2graphql.py
@@ -9,13 +9,13 @@
 import rich_click as click
 from vss_tools import log
 import vss_tools.vspec.cli_options as clo
-from vss_tools.vspec.vssexporters.utils import get_trees
-from vss_tools.vspec.model.vsstree import VSSNode
+from vss_tools.vspec.model import VSSDataDatatype
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.main import get_trees
 from pathlib import Path
-from vss_tools.vspec.utils.stringstyle import camel_back
-from vss_tools.vspec.model.constants import VSSDataType
-from vss_tools.vspec import VSpecError
+from vss_tools.vspec.utils.misc import camel_back
 from typing import Dict
+from vss_tools.vspec.datatypes import Datatypes
 
 from graphql import (
     GraphQLSchema,
@@ -32,34 +32,38 @@
 )
 
 GRAPHQL_TYPE_MAPPING = {
-    VSSDataType.INT8: GraphQLInt,
-    VSSDataType.INT8_ARRAY: GraphQLList(GraphQLInt),
-    VSSDataType.UINT8: GraphQLInt,
-    VSSDataType.UINT8_ARRAY: GraphQLList(GraphQLInt),
-    VSSDataType.INT16: GraphQLInt,
-    VSSDataType.INT16_ARRAY: GraphQLList(GraphQLInt),
-    VSSDataType.UINT16: GraphQLInt,
-    VSSDataType.UINT16_ARRAY: GraphQLList(GraphQLInt),
-    VSSDataType.INT32: GraphQLInt,
-    VSSDataType.INT32_ARRAY: GraphQLList(GraphQLInt),
-    VSSDataType.UINT32: GraphQLFloat,
-    VSSDataType.UINT32_ARRAY: GraphQLList(GraphQLFloat),
-    VSSDataType.INT64: GraphQLFloat,
-    VSSDataType.INT64_ARRAY: GraphQLList(GraphQLFloat),
-    VSSDataType.UINT64: GraphQLFloat,
-    VSSDataType.UINT64_ARRAY: GraphQLList(GraphQLFloat),
-    VSSDataType.FLOAT: GraphQLFloat,
-    VSSDataType.FLOAT_ARRAY: GraphQLList(GraphQLFloat),
-    VSSDataType.DOUBLE: GraphQLFloat,
-    VSSDataType.DOUBLE_ARRAY: GraphQLList(GraphQLFloat),
-    VSSDataType.BOOLEAN: GraphQLBoolean,
-    VSSDataType.BOOLEAN_ARRAY: GraphQLList(GraphQLBoolean),
-    VSSDataType.STRING: GraphQLString,
-    VSSDataType.STRING_ARRAY: GraphQLList(GraphQLString),
+    Datatypes.INT8[0]: GraphQLInt,
+    Datatypes.INT8_ARRAY[0]: GraphQLList(GraphQLInt),
+    Datatypes.UINT8[0]: GraphQLInt,
+    Datatypes.UINT8_ARRAY[0]: GraphQLList(GraphQLInt),
+    Datatypes.INT16[0]: GraphQLInt,
+    Datatypes.INT16_ARRAY[0]: GraphQLList(GraphQLInt),
+    Datatypes.UINT16[0]: GraphQLInt,
+    Datatypes.UINT16_ARRAY[0]: GraphQLList(GraphQLInt),
+    Datatypes.INT32[0]: GraphQLInt,
+    Datatypes.INT32_ARRAY[0]: GraphQLList(GraphQLInt),
+    Datatypes.UINT32[0]: GraphQLFloat,
+    Datatypes.UINT32_ARRAY[0]: GraphQLList(GraphQLFloat),
+    Datatypes.INT64[0]: GraphQLFloat,
+    Datatypes.INT64_ARRAY[0]: GraphQLList(GraphQLFloat),
+    Datatypes.UINT64[0]: GraphQLFloat,
+    Datatypes.UINT64_ARRAY[0]: GraphQLList(GraphQLFloat),
+    Datatypes.FLOAT[0]: GraphQLFloat,
+    Datatypes.FLOAT_ARRAY[0]: GraphQLList(GraphQLFloat),
+    Datatypes.DOUBLE[0]: GraphQLFloat,
+    Datatypes.DOUBLE_ARRAY[0]: GraphQLList(GraphQLFloat),
+    Datatypes.BOOLEAN[0]: GraphQLBoolean,
+    Datatypes.BOOLEAN_ARRAY[0]: GraphQLList(GraphQLBoolean),
+    Datatypes.STRING[0]: GraphQLString,
+    Datatypes.STRING_ARRAY[0]: GraphQLList(GraphQLString),
 }
 
 
-def get_schema_from_tree(root_node: VSSNode, additional_leaf_fields: list) -> str:
+class GraphQLFieldException(Exception):
+    pass
+
+
+def get_schema_from_tree(root: VSSNode, additional_leaf_fields: list) -> str:
     """Takes a VSSNode and additional fields for the leafs. Returns a graphql schema as string."""
     args = dict(
         id=GraphQLArgument(
@@ -78,32 +82,29 @@ def get_schema_from_tree(root_node: VSSNode, additional_leaf_fields: list) -> st
     root_query = GraphQLObjectType(
         "Query",
         lambda: {
-            "vehicle": GraphQLField(
-                to_gql_type(root_node, additional_leaf_fields), args
-            )
+            "vehicle": GraphQLField(to_gql_type(root, additional_leaf_fields), args)
         },
     )
     return print_schema(GraphQLSchema(root_query))
 
 
 def to_gql_type(node: VSSNode, additional_leaf_fields: list) -> GraphQLObjectType:
-    if node.has_datatype():
+    if isinstance(node.data, VSSDataDatatype):
         fields = leaf_fields(node, additional_leaf_fields)
     else:
         fields = branch_fields(node, additional_leaf_fields)
     return GraphQLObjectType(
-        name=node.qualified_name("_"),
+        name=node.get_fqn("_"),
         fields=fields,
-        description=node.description,
+        description=node.get_vss_data().description,
     )
 
 
 def leaf_fields(node: VSSNode, additional_leaf_fields: list) -> Dict[str, GraphQLField]:
     field_dict = {}
-    if node.datatype is not None:
-        field_dict["value"] = field(
-            node, "Value: ", GRAPHQL_TYPE_MAPPING[node.datatype]
-        )
+    datatype = getattr(node.data, "datatype", None)
+    if datatype is not None:
+        field_dict["value"] = field(node, "Value: ", GRAPHQL_TYPE_MAPPING[datatype])
     field_dict["timestamp"] = field(node, "Timestamp: ")
     if additional_leaf_fields:
         for additional_field in additional_leaf_fields:
@@ -112,10 +113,11 @@ def leaf_fields(node: VSSNode, additional_leaf_fields: list) -> Dict[str, GraphQ
                     node, f" {str(additional_field[1])}: "
                 )
             else:
-                raise VSpecError(
+                raise GraphQLFieldException(
                     "", "", "Incorrect format of graphql field specification"
                 )
-    if node.has_unit():
+    unit = getattr(node.data, "unit", None)
+    if unit:
         field_dict["unit"] = field(node, "Unit of ")
     return field_dict
 
@@ -123,8 +125,8 @@ def leaf_fields(node: VSSNode, additional_leaf_fields: list) -> Dict[str, GraphQ
 def branch_fields(
     node: VSSNode, additional_leaf_fields: list
 ) -> Dict[str, GraphQLField]:
-    # we only consider nodes that either have children or are leave with a datatype
-    valid = (c for c in node.children if len(c.children) or hasattr(c, "datatype"))
+    # we only consider nodes that either have children or are datatype leafs
+    valid = (c for c in node.children if len(c.children) or hasattr(c.data, "datatype"))
     return {
         camel_back(c.name): field(c, type=to_gql_type(c, additional_leaf_fields))
         for c in valid
@@ -132,10 +134,11 @@ def branch_fields(
 
 
 def field(node: VSSNode, description_prefix="", type=GraphQLString) -> GraphQLField:
+    data = node.get_vss_data()
     return GraphQLField(
         type,
-        deprecation_reason=node.deprecation or None,
-        description=f"{description_prefix}{node.description}",
+        deprecation_reason=data.deprecation,
+        description=f"{description_prefix}{data.description}",
     )
 
 
@@ -173,19 +176,15 @@ def cli(
     """
     Export as GraphQL.
     """
-    tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        False,
-        quantities,
-        vspec,
-        units,
-        tuple(),
-        None,
-        overlays,
-        True,
+    tree, _ = get_trees(
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        quantities=quantities,
+        units=units,
+        overlays=overlays,
     )
     log.info("Generating graphql output...")
     outfile = open(output, "w")
diff --git a/src/vss_tools/vspec/vssexporters/vss2id.py b/src/vss_tools/vspec/vssexporters/vss2id.py
index b42e1286..b9f77fdb 100644
--- a/src/vss_tools/vspec/vssexporters/vss2id.py
+++ b/src/vss_tools/vspec/vssexporters/vss2id.py
@@ -10,18 +10,17 @@
 # Generate IDs of 4bytes size, 3 bytes incremental value + 1 byte for layer id.
 
 import sys
-from typing import Dict, Tuple
+from typing import Dict, Tuple, Any
 
 import yaml
 
-from vss_tools.vspec.vssexporters.utils import get_trees
+from vss_tools.vspec.utils.misc import getattr_nn
+from vss_tools.vspec.main import get_trees
 import rich_click as click
 import vss_tools.vspec.cli_options as clo
 from pathlib import Path
 from vss_tools import log
-from vss_tools.vspec import load_tree
-from vss_tools.vspec.model.constants import VSSTreeType
-from vss_tools.vspec.model.vsstree import VSSNode
+from vss_tools.vspec.tree import VSSNode
 from vss_tools.vspec.utils import vss2id_val
 from vss_tools.vspec.utils.idgen_utils import (
     fnv1_32_hash,
@@ -41,18 +40,27 @@ def generate_split_id(
     @return: tuple of hashed string and id counter
     """
 
-    if hasattr(node, "fka") and node.fka:
-        name = node.fka[0] if isinstance(node.fka, list) else node.fka
+    fka = getattr(node.data, "fka", None)
+    if fka:
+        name = fka[0] if isinstance(fka, list) else fka
     else:
-        name = node.qualified_name()
+        name = node.get_fqn()
+    data = node.get_vss_data()
+    datatype = getattr_nn(data, "datatype", "")
+    unit = getattr_nn(data, "unit", "")
+    allowed = getattr_nn(data, "allowed", "")
+    if allowed == []:
+        allowed = ""
+    min = getattr_nn(data, "min", "")
+    max = getattr_nn(data, "max", "")
     identifier = get_node_identifier_bytes(
         name,
-        node.data_type_str,
-        node.type.value,
-        node.get_unit(),
-        node.allowed,
-        node.min,
-        node.max,
+        datatype,
+        data.type.value,
+        unit,
+        allowed,
+        min,
+        max,
         strict_mode,
     )
     hashed_str = format(fnv1_32_hash(identifier), "08X")
@@ -60,10 +68,12 @@ def generate_split_id(
     return hashed_str, id_counter + 1
 
 
-def export_node(yaml_dict, node, id_counter, strict_mode: bool) -> Tuple[int, int]:
+def export_node(
+    data: dict[str, Any], node: VSSNode, id_counter, strict_mode: bool
+) -> Tuple[int, int]:
     """Recursive function to export the full tree to a dict
 
-    @param yaml_dict: the to be exported dict
+    @param data: the to be exported dict
     @param node: parent node of the tree
     @param id_counter: counter for amount of ids
     @param strict_mode: strict mode means case sensitivity for static UID generation
@@ -71,58 +81,58 @@ def export_node(yaml_dict, node, id_counter, strict_mode: bool) -> Tuple[int, in
     """
 
     node_id: str
-    if not node.constUID:
-        node_id, id_counter = generate_split_id(node, id_counter, strict_mode)
-        node_id = f"0x{node_id}"
-    else:
+    node_data = node.get_vss_data()
+    if node_data.constUID:
         log.info(
-            f"Using const ID for {node.qualified_name()}. If you didn't mean "
+            f"Using const ID for {node.get_fqn()}. If you didn't mean "
             "to do that you can remove it in your vspec / overlay."
         )
-        node_id = node.constUID
-
-    assert node_id.startswith("0x"), f"Node ID has to begin with '0x': {node_id}"
-    assert len(node_id) == 10, f"Invalid node ID: {node_id}"
+        node_id = node_data.constUID
+    else:
+        node_id, id_counter = generate_split_id(node, id_counter, strict_mode)
+        node_id = f"0x{node_id}"
 
     # check for hash duplicates
-    for key, value in get_all_keys_values(yaml_dict):
+    for key, value in get_all_keys_values(data):
         if not isinstance(value, dict) and key == "staticUID":
             if node_id == value:
                 log.fatal(
                     f"There is a small chance that the result of FNV-1 "
                     f"hashes are the same in this case the hash of node "
-                    f"'{node.qualified_name()}' is the same as another hash."
+                    f"'{node.get_fqn()}' is the same as another hash."
                     f"Can you please update it."
                 )
                 # We could add handling of duplicates here
                 sys.exit(-1)
 
-    node_path = node.qualified_name()
-
-    yaml_dict[node_path] = {"staticUID": f"{node_id}"}
-    yaml_dict[node_path]["description"] = node.description
-    yaml_dict[node_path]["type"] = str(node.type.value)
-    if node.unit:
-        yaml_dict[node_path]["unit"] = str(node.unit.value)
-    if node.is_signal() or node.is_property():
-        yaml_dict[node_path]["datatype"] = node.data_type_str
-    if node.allowed:
-        yaml_dict[node_path]["allowed"] = node.allowed
-    if isinstance(node.min, int | float):
-        yaml_dict[node_path]["min"] = node.min
-    if isinstance(node.max, int | float):
-        yaml_dict[node_path]["max"] = node.max
-
-    if node.fka:
-        yaml_dict[node_path]["fka"] = node.fka
-    elif "fka" in node.extended_attributes.keys():
-        yaml_dict[node_path]["fka"] = node.extended_attributes["fka"]
-
-    if node.deprecation:
-        yaml_dict[node_path]["deprecation"] = node.deprecation
+    node_path = node.get_fqn()
+
+    data[node_path] = {"staticUID": f"{node_id}"}
+    data[node_path]["description"] = node_data.description
+    data[node_path]["type"] = str(node_data.type.value)
+    if getattr(node_data, "unit", None):
+        data[node_path]["unit"] = getattr(node_data, "unit")
+    if hasattr(node_data, "datatype"):
+        data[node_path]["datatype"] = getattr(node_data, "datatype")
+    if getattr(node_data, "allowed", None):
+        data[node_path]["allowed"] = getattr(node_data, "allowed")
+
+    min = getattr(node_data, "min", None)
+    if min is not None:
+        data[node_path]["min"] = min
+    max = getattr(node_data, "max", None)
+    if max is not None:
+        data[node_path]["max"] = max
+
+    fka = getattr(node_data, "fka", None)
+    if fka:
+        data[node_path]["fka"] = fka
+
+    if node_data.deprecation:
+        data[node_path]["deprecation"] = node_data.deprecation
 
     for child in node.children:
-        id_counter, id_counter = export_node(yaml_dict, child, id_counter, strict_mode)
+        id_counter, id_counter = export_node(data, child, id_counter, strict_mode)
 
     return id_counter, id_counter
 
@@ -140,10 +150,9 @@ def export_node(yaml_dict, node, id_counter, strict_mode: bool) -> Tuple[int, in
 @clo.quantities_opt
 @clo.units_opt
 @clo.types_opt
-@clo.types_output_opt
 @click.option(
     "--validate-static-uid",
-    type=click.Path(dir_okay=False, readable=True, exists=True),
+    type=click.Path(dir_okay=False, readable=True, exists=True, path_type=Path),
     help="Validation file.",
 )
 @click.option("--validate-only", is_flag=True, help="Only validating. Not exporting.")
@@ -165,7 +174,6 @@ def cli(
     quantities: tuple[Path],
     units: tuple[Path],
     types: tuple[Path],
-    types_output: Path,
     validate_static_uid: Path,
     validate_only: bool,
     case_sensitive: bool,
@@ -173,19 +181,18 @@ def cli(
     """
     Export as IDs.
     """
-    tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        types,
-        types_output,
-        overlays,
-        expand,
+    tree, _ = get_trees(
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
+        expand=expand,
     )
     log.info("Generating vspec output including static UIDs...")
 
@@ -199,8 +206,17 @@ def cli(
             f"file '{validate_static_uid}'"
         )
 
-        validation_tree = load_tree(
-            str(validate_static_uid), ["."], tree_type=VSSTreeType.SIGNAL_TREE
+        validation_tree, _ = get_trees(
+            vspec=validate_static_uid,
+            include_dirs=include_dirs,
+            aborts=aborts,
+            strict=strict,
+            extended_attributes=extended_attributes,
+            uuid=uuid,
+            quantities=quantities,
+            units=units,
+            types=types,
+            expand=expand,
         )
         vss2id_val.validate_static_uids(signals_yaml_dict, validation_tree, strict)
 
diff --git a/src/vss_tools/vspec/vssexporters/vss2json.py b/src/vss_tools/vspec/vssexporters/vss2json.py
index 5dd00ab0..fcfced01 100644
--- a/src/vss_tools/vspec/vssexporters/vss2json.py
+++ b/src/vss_tools/vspec/vssexporters/vss2json.py
@@ -8,83 +8,25 @@
 
 # Convert vspec tree to JSON
 
-from vss_tools.vspec.model.vsstree import VSSNode
+from vss_tools.vspec.tree import VSSNode
 import json
+from typing import Any
 import rich_click as click
 import vss_tools.vspec.cli_options as clo
-from vss_tools.vspec.vssexporters.utils import get_trees
+from vss_tools.vspec.main import get_trees
 from pathlib import Path
-from typing import Dict, Any
 from vss_tools import log
 
 
-def export_node(
-    json_dict, node, print_uuid, all_extended_attributes: bool, expand: bool
-):
-    json_dict[node.name] = {}
-
-    if node.is_signal() or node.is_property():
-        json_dict[node.name]["datatype"] = node.data_type_str
-
-    json_dict[node.name]["type"] = str(node.type.value)
-
-    # many optional attributes are initilized to "" in vsstree.py
-    if node.min is not None:
-        json_dict[node.name]["min"] = node.min
-    if node.max is not None:
-        json_dict[node.name]["max"] = node.max
-    if node.allowed != "":
-        json_dict[node.name]["allowed"] = node.allowed
-    if node.default != "":
-        json_dict[node.name]["default"] = node.default
-    if node.deprecation != "":
-        json_dict[node.name]["deprecation"] = node.deprecation
-
-    # in case of unit or aggregate, the attribute will be missing
-    try:
-        json_dict[node.name]["unit"] = str(node.unit.value)
-    except AttributeError:
-        pass
-    try:
-        json_dict[node.name]["aggregate"] = node.aggregate
-    except AttributeError:
-        pass
-
-    json_dict[node.name]["description"] = node.description
-    if node.comment != "":
-        json_dict[node.name]["comment"] = node.comment
-
-    if print_uuid:
-        json_dict[node.name]["uuid"] = node.uuid
-
-    for k, v in node.extended_attributes.items():
-        if (
-            not all_extended_attributes
-            and k not in VSSNode.whitelisted_extended_attributes
-        ):
-            continue
-        json_dict[node.name][k] = v
-
-    # Include instance information if we run tool in "no-expand" mode
-    if not expand and node.instances is not None:
-        json_dict[node.name]["instances"] = node.instances
-
-    # Might be better to not generate child dict, if there are no children
-    # if node.type == VSSType.BRANCH and len(node.children) != 0:
-    #    json_dict[node.name]["children"]={}
-
-    # But old JSON code always generates children, so lets do so to
-    if node.is_branch() or node.is_struct():
-        json_dict[node.name]["children"] = {}
-
+def get_data(node: VSSNode, with_extra_attributes: bool = True):
+    data = node.data.as_dict(with_extra_attributes)
+    if node.uuid:
+        data["uuid"] = node.uuid
+    if len(node.children) > 0:
+        data["children"] = {}
     for child in node.children:
-        export_node(
-            json_dict[node.name]["children"],
-            child,
-            print_uuid,
-            all_extended_attributes,
-            expand,
-        )
+        data["children"][child.name] = get_data(child)
+    return data
 
 
 @click.command()
@@ -101,8 +43,8 @@ def export_node(
 @clo.units_opt
 @clo.types_opt
 @clo.types_output_opt
-@clo.extend_all_attributes_opt
 @clo.pretty_print_opt
+@clo.extend_all_attributes_opt
 def cli(
     vspec: Path,
     output: Path,
@@ -116,49 +58,43 @@ def cli(
     quantities: tuple[Path],
     units: tuple[Path],
     types: tuple[Path],
-    types_output: Path,
-    extend_all_attributes: bool,
+    types_output: Path | None,
     pretty: bool,
+    extend_all_attributes: bool,
 ):
     """
     Export as JSON.
     """
     tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        types,
-        types_output,
-        overlays,
-        expand,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
+        expand=expand,
     )
     log.info("Generating JSON output...")
     indent = None
     if pretty:
-        log.info("Serializing pretty JSON...")
         indent = 2
-    else:
-        log.info("Serializing compact JSON...")
 
-    signals_json_dict: Dict[str, Any] = {}
-    export_node(signals_json_dict, tree, uuid, extend_all_attributes, expand)
+    signals_data = {tree.name: get_data(tree, extend_all_attributes)}
 
     if datatype_tree:
-        data_types_json_dict: Dict[str, Any] = {}
-        export_node(
-            data_types_json_dict, datatype_tree, uuid, extend_all_attributes, expand
-        )
+        types_data: dict[str, Any] = {
+            datatype_tree.name: get_data(datatype_tree, extend_all_attributes)
+        }
         if not types_output:
             log.info("Adding custom data types to signal dictionary")
-            signals_json_dict["ComplexDataTypes"] = data_types_json_dict
+            signals_data["ComplexDataTypes"] = types_data
         else:
             with open(types_output, "w") as f:
-                json.dump(data_types_json_dict, f, indent=indent, sort_keys=True)
+                json.dump(types_data, f, indent=indent, sort_keys=True)
 
     with open(output, "w") as f:
-        json.dump(signals_json_dict, f, indent=indent, sort_keys=True)
+        json.dump(signals_data, f, indent=indent, sort_keys=True)
diff --git a/src/vss_tools/vspec/vssexporters/vss2jsonschema.py b/src/vss_tools/vspec/vssexporters/vss2jsonschema.py
index 45f285f3..2c2be8b4 100644
--- a/src/vss_tools/vspec/vssexporters/vss2jsonschema.py
+++ b/src/vss_tools/vspec/vssexporters/vss2jsonschema.py
@@ -11,7 +11,9 @@
 
 import json
 import rich_click as click
-from vss_tools.vspec.vssexporters.utils import get_trees
+from vss_tools.vspec.model import VSSDataBranch, VSSDataDatatype, VSSDataStruct
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.main import get_trees
 import vss_tools.vspec.cli_options as clo
 from pathlib import Path
 from vss_tools import log
@@ -47,66 +49,71 @@
 
 def export_node(
     json_dict,
-    node,
-    print_uuid,
+    node: VSSNode,
+    print_uuid: bool,
     all_extended_attributes: bool,
     no_additional_properties: bool,
     require_all_properties: bool,
 ):
     """Preparing nodes for JSON schema output."""
     # keyword with X- sign are left for extensions and they are not part of official JSON schema
+    data = node.get_vss_data()
     json_dict[node.name] = {
-        "description": node.description,
+        "description": data.description,
     }
 
-    if node.is_signal() or node.is_property():
-        json_dict[node.name]["type"] = type_map[node.data_type_str]
-
-    # many optional attributes are initialized to "" in vsstree.py
-    if node.min is not None:
-        json_dict[node.name]["minimum"] = node.min
-    if node.max is not None:
-        json_dict[node.name]["maximum"] = node.max
-    if node.allowed != "":
-        json_dict[node.name]["enum"] = node.allowed
-    if node.default != "":
-        json_dict[node.name]["default"] = node.default
-    if node.is_struct():
-        # change type to object
-        json_dict[node.type]["type"] = "object"
+    if isinstance(data, VSSDataDatatype):
+        json_dict[node.name]["type"] = type_map[data.datatype]
+
+    min = getattr(data, "min", None)
+    if min is not None:
+        json_dict[node.name]["minimum"] = min
+
+    max = getattr(data, "max", None)
+    if max is not None:
+        json_dict[node.name]["maximum"] = max
+
+    allowed = getattr(data, "allowed", None)
+    if allowed:
+        json_dict[node.name]["enum"] = allowed
+
+    default = getattr(data, "default", None)
+    if default:
+        json_dict[node.name]["default"] = default
+
+    if isinstance(data, VSSDataStruct):
+        json_dict[node.name]["type"] = "object"
 
     if all_extended_attributes:
-        if node.type != "":
-            json_dict[node.name]["x-VSStype"] = str(node.type.value)
-        if node.data_type_str != "":
-            json_dict[node.name]["x-datatype"] = node.data_type_str
-        if node.deprecation != "":
-            json_dict[node.name]["x-deprecation"] = node.deprecation
+        json_dict[node.name]["x-VSStype"] = data.type.value
+        datatype = getattr(data, "datatype", None)
+        if datatype:
+            json_dict[node.name]["x-datatype"] = datatype
+        if data.deprecation:
+            json_dict[node.name]["x-deprecation"] = data.deprecation
 
         # in case of unit or aggregate, the attribute will be missing
-        try:
-            json_dict[node.name]["x-unit"] = str(node.unit.value)
-        except AttributeError:
-            pass
-        try:
-            json_dict[node.name]["x-aggregate"] = node.aggregate
-            if node.aggregate:
-                # change type to object
-                json_dict[node.type]["type"] = "object"
-        except AttributeError:
-            pass
-
-        if node.comment != "":
-            json_dict[node.name]["x-comment"] = node.comment
+        unit = getattr(data, "unit", None)
+        if unit:
+            json_dict[node.name]["x-unit"] = unit
+
+        aggregate = getattr(data, "aggregate", None)
+        if aggregate:
+            json_dict[node.name]["x-aggregate"] = aggregate
+            if aggregate:
+                json_dict[node.name]["type"] = "object"
+
+        if data.comment:
+            json_dict[node.name]["x-comment"] = data.comment
 
         if print_uuid:
             json_dict[node.name]["x-uuid"] = node.uuid
 
-    for k, v in node.extended_attributes.items():
-        json_dict[node.name][k] = v
+    for field in data.get_extra_attributes():
+        json_dict[node.name][field] = getattr(data, field)
 
     # Generate child nodes
-    if node.is_branch() or node.is_struct():
+    if isinstance(data, VSSDataBranch) or isinstance(node.data, VSSDataStruct):
         if no_additional_properties:
             json_dict[node.name]["additionalProperties"] = False
         json_dict[node.name]["properties"] = {}
@@ -136,7 +143,6 @@ def export_node(
 @clo.quantities_opt
 @clo.units_opt
 @clo.types_opt
-@clo.types_output_opt
 @clo.extend_all_attributes_opt
 @clo.pretty_print_opt
 @click.option(
@@ -162,7 +168,6 @@ def cli(
     quantities: tuple[Path],
     units: tuple[Path],
     types: tuple[Path],
-    types_output: Path,
     extend_all_attributes: bool,
     pretty: bool,
     no_additional_properties: bool,
@@ -172,18 +177,17 @@ def cli(
     Export as a jsonschema.
     """
     tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        types,
-        types_output,
-        overlays,
-        expand,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
+        expand=expand,
     )
     log.info("Generating JSON schema...")
     indent = None
diff --git a/src/vss_tools/vspec/vssexporters/vss2protobuf.py b/src/vss_tools/vspec/vssexporters/vss2protobuf.py
index 192ad24b..5b415014 100644
--- a/src/vss_tools/vspec/vssexporters/vss2protobuf.py
+++ b/src/vss_tools/vspec/vssexporters/vss2protobuf.py
@@ -9,21 +9,21 @@
 # Convert vspec file to proto
 #
 
-import os
+from io import TextIOWrapper
 import sys
 import rich_click as click
 import vss_tools.vspec.cli_options as clo
-from vss_tools.vspec.vssexporters.utils import get_trees
+from vss_tools.vspec.model import (
+    VSSDataDatatype,
+    VSSDataStruct,
+    VSSDataBranch,
+)
+from vss_tools.vspec.main import get_trees
+from vss_tools.vspec.tree import VSSNode
 from pathlib import Path
-from typing import Set
 
-from anytree import PreOrderIter  # type: ignore[import]
+from anytree import findall
 from vss_tools import log
-from vss_tools.vspec.model.vsstree import VSSNode
-
-# Add path to main py vspec  parser
-myDir = os.path.dirname(os.path.realpath(__file__))
-sys.path.append(os.path.join(myDir, ".."))
 
 PATH_DELIMITER = "."
 DIR_DELIMITER = "/"
@@ -37,130 +37,123 @@
 }
 
 
-class ProtoExporter(object):
-    def __init__(self, out_dir: Path):
-        self.out_files: Set[Path] = set()
-        self.out_dir = out_dir
-
-    def setup_file(self, fp: Path, package_name: str):
-        with open(fp, "w") as proto_file:
-            log.info(f"Initializing {fp}, package {package_name}")
-            proto_file.write('syntax = "proto3";\n\n')
-            # set up the proto's package
-            proto_file.write(f"package {package_name};\n\n")
-            self.out_files.add(fp)
-
-    def traverse_data_type_tree(self, tree: VSSNode, static_uid, add_optional):
-        """
-        All structs in a branch are written to a single .proto file.
-        The file's base name is same as the branch's name
-        The fully qualified path of the type becomes the package.
-
-        Example A.B.C.MyType becomes:
-        "package A.B.C;
-        message MyType{}"
-
-        The files are organized by package.
-        With the above example, the file generated is
-        A/B/C/C.proto
-        """
-
-        tree_node: VSSNode
-
-        for tree_node in filter(lambda n: n.is_struct(), PreOrderIter(tree)):
-            type_qn = tree_node.qualified_name(PATH_DELIMITER).split(PATH_DELIMITER)
-            curr_branch_qn = type_qn[:-1]
-            # create dir if necessary
-            dp = self.out_dir / Path(DIR_DELIMITER.join(curr_branch_qn))
-            dp.mkdir(parents=True, exist_ok=True)
-
-            curr_branch = type_qn[-2]
-            fp = dp / Path(f"{curr_branch}.proto")
-            is_first_write = fp not in self.out_files
-            if is_first_write:
-                self.setup_file(fp, ".".join(curr_branch_qn))
-            with open(fp, "a") as proto_file:
-                # import of other proto types in other branches
-                imports = []
-                for c_node in filter(
-                    lambda c: c.is_property() and not c.has_datatype(),
-                    tree_node.children,
-                ):
-                    child_branch_path = c_node.get_datatype().split(PATH_DELIMITER)[:-1]
-                    child_branch_path_str = DIR_DELIMITER.join(child_branch_path)
-                    if child_branch_path_str != DIR_DELIMITER.join(curr_branch_qn):
-                        child_basename = child_branch_path[-1]
-                        imports.append(
-                            f"{child_branch_path_str}/{child_basename}.proto"
-                        )
-
-                # unique sorted list
-                imports = list(set(imports))
-                imports.sort()
-                for importstmt in imports:
-                    proto_file.write(f'import "{importstmt}";\n')
-                proto_file.write("\n")
-
-                proto_file.write(f"message {type_qn[-1]} {{" + "\n")
-                print_message_body(
-                    tree_node.children, proto_file, static_uid, add_optional
-                )
-                proto_file.write("}\n\n")
-                log.info(f"Wrote {type_qn[-1]} to {fp}")
+def init_package_file(path: Path, package_name: str):
+    with open(path, "w") as f:
+        log.info(f"Initializing {path}, package {package_name}")
+        f.write('syntax = "proto3";\n\n')
+        f.write(f"package {package_name};\n\n")
+
+
+def traverse_data_type_tree(
+    tree: VSSNode, static_uid: bool, add_optional: bool, out_dir: Path
+):
+    """
+    All structs in a branch are written to a single .proto file.
+    The file's base name is same as the branch's name
+    The fully qualified path of the type becomes the package.
+
+    Example A.B.C.MyType becomes:
+    "package A.B.C;
+    message MyType{}"
+
+    The files are organized by package.
+    With the above example, the file generated is
+    A/B/C/C.proto
+    """
+
+    node: VSSNode
+    for node in findall(tree, filter_=lambda n: isinstance(n.data, VSSDataStruct)):
+        # A/B/C/MyType
+        struct_path = Path(node.get_fqn("/"))
+
+        # <out-dir>/A/B/C
+        package_path = out_dir / struct_path.parent
+        package_path.mkdir(parents=True, exist_ok=True)
+
+        # <out-dir>/A/B/C/C.proto
+        out_file = package_path / f"{package_path.name}.proto"
+        if not out_file.exists():
+            init_package_file(out_file, ".".join(struct_path.parent.parts))
+
+        with open(out_file, "a") as fd:
+            imports = []
+            for c_node in findall(
+                node, filter_=lambda n: isinstance(n.data, VSSDataDatatype)
+            ):
+                datatype = c_node.data.datatype
+                if "." not in datatype:
+                    continue
+                c_struct_path = Path(datatype.replace(".", "/"))
+                if c_struct_path.parent != struct_path.parent:
+                    imports.append(
+                        f"{c_struct_path.parent}/{c_struct_path.parent.name}.proto"
+                    )
+
+            write_imports(fd, imports)
+
+            fd.write(f"message {struct_path.name} {{" + "\n")
+            print_messages(node.children, fd, static_uid, add_optional)
+            fd.write("}\n\n")
+            log.info(f"Wrote {struct_path.name} to {out_file}")
 
 
-def traverse_signal_tree(tree: VSSNode, proto_file, static_uid, add_optional):
-    proto_file.write('syntax = "proto3";\n\n')
-    tree_node: VSSNode
+def traverse_signal_tree(
+    tree: VSSNode, fd: TextIOWrapper, static_uid: bool, add_optional: bool
+):
+    fd.write('syntax = "proto3";\n\n')
 
-    # get all the import statements for dependent types (structs)
     imports = []
-    for c_node in filter(
-        lambda c: c.is_signal() and not c.has_datatype(), PreOrderIter(tree)
+    for node in findall(tree, filter_=lambda n: isinstance(n.data, VSSDataDatatype)):
+        datatype = node.data.datatype
+        if "." not in datatype:
+            continue
+        struct_path = Path(node.data.datatype.replace(".", "/"))  # type: ignore
+        imports.append(f"{struct_path.parent}/{struct_path.parent.name}.proto")
+    write_imports(fd, imports)
+
+    # write proto messages to file
+    for node in findall(
+        tree, filter_=lambda node: isinstance(node.data, VSSDataBranch)
     ):
-        child_path = c_node.get_datatype().split(PATH_DELIMITER)[:-1]
-        child_basename = child_path[-1]
-        imports.append(f"{DIR_DELIMITER.join(child_path)}/{child_basename}.proto")
+        fd.write(f"message {node.get_fqn('')} {{" + "\n")
+        print_messages(node.children, fd, static_uid, add_optional)
+        fd.write("}\n\n")
 
-    # unique sorted list
+
+def write_imports(fd: TextIOWrapper, imports: list[str]):
     imports = list(set(imports))
     imports.sort()
-    # write import statements to file
-    for importstmt in imports:
-        proto_file.write(f'import "{importstmt}";\n')
-    proto_file.write("\n")
-
-    # write proto messages to file
-    for tree_node in filter(lambda n: n.is_branch(), PreOrderIter(tree)):
-        proto_file.write(f"message {tree_node.qualified_name('')} {{" + "\n")
-        print_message_body(tree_node.children, proto_file, static_uid, add_optional)
-        proto_file.write("}\n\n")
+    for stmt in imports:
+        fd.write(f'import "{stmt}";\n')
+    fd.write("\n")
 
 
-def print_message_body(nodes, proto_file, static_uid, add_optional):
-    usedKeys = {}
+def print_messages(
+    nodes: tuple[VSSNode], fd: TextIOWrapper, static_uid: bool, add_optional: bool
+):
+    usedKeys: dict[int, str] = {}
     for i, node in enumerate(nodes, 1):
-        if not (node.is_branch() or node.is_struct()):
-            dt_val = node.get_datatype()
+        if isinstance(node.data, VSSDataDatatype):
+            dt_val = node.data.datatype
             data_type = mapped.get(dt_val.strip("[]"), dt_val.strip("[]"))
             if dt_val.endswith("[]"):
                 data_type = "repeated " + data_type
             elif add_optional:
                 data_type = "optional " + data_type
         else:
-            data_type = node.qualified_name("")
+            data_type = node.get_fqn("")
             if add_optional:
                 data_type = "optional " + data_type
         if static_uid:
-            if "staticUID" not in node.extended_attributes:
+            if "staticUID" not in node.data.get_extra_attributes():
                 log.fatal(
                     (
-                        f"Aborting because {node.qualified_name('.')} does not have the staticUID attribute. "
+                        f"Aborting because {node.get_fqn()} does not have the staticUID attribute. "
                         f"When using the option --static-uid each node must have the attribute staticUID."
                     )
                 )
                 sys.exit(-1)
-            fieldNumber = int(int(node.extended_attributes["staticUID"], 0) / 8)
+            fieldNumber = int(int(getattr(node.data, "staticUID"), 0) / 8)
             if fieldNumber < 20000 and fieldNumber >= 19000:
                 log.fatal("""Aborting because field number {fieldNumber} for signal {node.name} is in
                 reservered range between 19000 and 20000. Consider changing the signal to alter the staticUID.""")
@@ -169,17 +162,17 @@ def print_message_body(nodes, proto_file, static_uid, add_optional):
                 log.fatal(
                     (
                         f"Aborting, due to collision for fieldNumber {fieldNumber}. "
-                        f"It is used by {node.qualified_name('.')} and {usedKeys[fieldNumber]}. "
+                        f"It is used by {node.get_fqn()} and {usedKeys[fieldNumber]}. "
                         "Consider changing the signals to alter the staticUID."
                     )
                 )
-                proto_file.truncate(0)
+                fd.truncate(0)
                 sys.exit(-1)
             else:
-                usedKeys[fieldNumber] = node.qualified_name(".")
+                usedKeys[fieldNumber] = node.get_fqn()
         else:
             fieldNumber = i
-        proto_file.write(f"  {data_type} {node.name} = {fieldNumber};" + "\n")
+        fd.write(f"  {data_type} {node.name} = {fieldNumber};" + "\n")
 
 
 @click.command()
@@ -215,7 +208,7 @@ def cli(
     quantities: tuple[Path],
     units: tuple[Path],
     types: tuple[Path],
-    types_out_dir: Path,
+    types_out_dir: Path | None,
     static_uid: bool,
     add_optional: bool,
 ):
@@ -224,27 +217,23 @@ def cli(
     """
     log.info("Generating protobuf output...")
     tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        False,
-        quantities,
-        vspec,
-        units,
-        types,
-        None,
-        overlays,
-        True,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
     )
-    if datatype_tree is not None:
-        if types_out_dir:
-            exporter_path = types_out_dir
-        else:
-            exporter_path = Path.cwd()
-        log.debug(f"Will use {exporter_path} for type exports")
-        exporter = ProtoExporter(exporter_path)
-        exporter.traverse_data_type_tree(datatype_tree, static_uid, add_optional)
+    if datatype_tree:
+        if not types_out_dir:
+            types_out_dir = Path.cwd()
+            log.warning(
+                f"No output directory given. Writing to: {types_out_dir.absolute()}"
+            )
+        traverse_data_type_tree(datatype_tree, static_uid, add_optional, types_out_dir)
 
     with open(output, "w") as f:
         log.info(f"Writing to: {output}")
diff --git a/src/vss_tools/vspec/vssexporters/vss2tree.py b/src/vss_tools/vspec/vssexporters/vss2tree.py
new file mode 100644
index 00000000..ca284cdb
--- /dev/null
+++ b/src/vss_tools/vspec/vssexporters/vss2tree.py
@@ -0,0 +1,89 @@
+# Copyright (c) 2024 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+
+import vss_tools.vspec.cli_options as clo
+from vss_tools.vspec.main import get_trees
+import rich_click as click
+from pathlib import Path
+from anytree import RenderTree
+from vss_tools import log
+
+from vss_tools.vspec.tree import VSSNode
+
+
+def get_rendered_tree(tree: VSSNode, attributes: tuple[str]) -> str:
+    tree_content_lines = []
+    for pre, fill, node in RenderTree(tree):
+        tree_content_lines.append("%s%s" % (pre, node.name))
+        for attribute in attributes:
+            content = getattr(node.data, attribute, None)
+            if content is None:
+                continue
+            if isinstance(content, str):
+                tree_content_lines.append("%s%s='%s'" % (fill, attribute, content))
+            else:
+                tree_content_lines.append("%s%s=%s" % (fill, attribute, content))
+    return "\n".join(tree_content_lines)
+
+
+@click.command()
+@clo.vspec_opt
+@clo.output_opt
+@clo.include_dirs_opt
+@clo.extended_attributes_opt
+@clo.strict_opt
+@clo.aborts_opt
+@clo.uuid_opt
+@clo.expand_opt
+@clo.overlays_opt
+@clo.quantities_opt
+@clo.units_opt
+@clo.types_opt
+@click.option("--attr", help="Show VSSData attribute", multiple=True)
+def cli(
+    vspec: Path,
+    include_dirs: tuple[Path],
+    extended_attributes: tuple[str],
+    strict: bool,
+    aborts: tuple[str],
+    uuid: bool,
+    expand: bool,
+    overlays: tuple[Path],
+    quantities: tuple[Path],
+    units: tuple[Path],
+    types: tuple[Path],
+    output: Path | None,
+    attr: tuple[str],
+):
+    """
+    Export as Tree.
+    """
+    tree, datatype_tree = get_trees(
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
+        expand=expand,
+    )
+
+    rendered_tree = get_rendered_tree(tree, attr)
+    if datatype_tree:
+        rendered_tree += "\n" + get_rendered_tree(datatype_tree, attr)
+
+    if output:
+        log.info(f"Writing tree to: {output.absolute()}")
+        with open(output, "w") as f:
+            f.write(rendered_tree)
+    else:
+        log.info(rendered_tree)
diff --git a/src/vss_tools/vspec/vssexporters/vss2yaml.py b/src/vss_tools/vspec/vssexporters/vss2yaml.py
index 95b4a508..17b0667c 100644
--- a/src/vss_tools/vspec/vssexporters/vss2yaml.py
+++ b/src/vss_tools/vspec/vssexporters/vss2yaml.py
@@ -11,72 +11,14 @@
 #
 
 
-from vss_tools.vspec.model.vsstree import VSSNode
 import yaml
 import rich_click as click
 import vss_tools.vspec.cli_options as clo
 from pathlib import Path
-from vss_tools.vspec.vssexporters.utils import get_trees
-from typing import Dict, Any
+from vss_tools.vspec.main import get_trees
 from vss_tools import log
 
 
-def export_node(yaml_dict, node, print_uuid, extend_all_attributes: bool, expand: bool):
-    node_path = node.qualified_name()
-
-    yaml_dict[node_path] = {}
-
-    yaml_dict[node_path]["type"] = str(node.type.value)
-
-    if node.is_signal() or node.is_property():
-        yaml_dict[node_path]["datatype"] = node.get_datatype()
-
-    # many optional attributes are initilized to "" in vsstree.py
-    if node.min is not None:
-        yaml_dict[node_path]["min"] = node.min
-    if node.max is not None:
-        yaml_dict[node_path]["max"] = node.max
-    if node.allowed != "":
-        yaml_dict[node_path]["allowed"] = node.allowed
-    if node.default != "":
-        yaml_dict[node_path]["default"] = node.default
-    if node.deprecation != "":
-        yaml_dict[node_path]["deprecation"] = node.deprecation
-
-    # in case of unit or aggregate, the attribute will be missing
-    try:
-        yaml_dict[node_path]["unit"] = str(node.unit.value)
-    except AttributeError:
-        pass
-    try:
-        yaml_dict[node_path]["aggregate"] = node.aggregate
-    except AttributeError:
-        pass
-
-    yaml_dict[node_path]["description"] = node.description
-
-    if node.comment != "":
-        yaml_dict[node_path]["comment"] = node.comment
-
-    if print_uuid:
-        yaml_dict[node_path]["uuid"] = node.uuid
-
-    for k, v in node.extended_attributes.items():
-        if (
-            not extend_all_attributes
-            and k not in VSSNode.whitelisted_extended_attributes
-        ):
-            continue
-        yaml_dict[node_path][k] = v
-
-    # Include instance information if we run tool in "no-expand" mode
-    if not expand and node.instances is not None:
-        yaml_dict[node_path]["instances"] = node.instances
-
-    for child in node.children:
-        export_node(yaml_dict, child, print_uuid, extend_all_attributes, expand)
-
-
 def export_yaml(file_name, content_dict):
     with open(file_name, "w") as f:
         yaml.dump(
@@ -131,40 +73,34 @@ def cli(
     quantities: tuple[Path],
     units: tuple[Path],
     types: tuple[Path],
-    types_output: Path,
+    types_output: Path | None,
     extend_all_attributes: bool,
 ):
     """
     Export as YAML.
     """
     tree, datatype_tree = get_trees(
-        include_dirs,
-        aborts,
-        strict,
-        extended_attributes,
-        uuid,
-        quantities,
-        vspec,
-        units,
-        types,
-        types_output,
-        overlays,
-        expand,
+        vspec=vspec,
+        include_dirs=include_dirs,
+        aborts=aborts,
+        strict=strict,
+        extended_attributes=extended_attributes,
+        uuid=uuid,
+        quantities=quantities,
+        units=units,
+        types=types,
+        overlays=overlays,
+        expand=expand,
     )
     log.info("Generating YAML output...")
+    tree_data = tree.as_flat_dict(extend_all_attributes)
 
-    signals_yaml_dict: Dict[str, Any] = {}
-    export_node(signals_yaml_dict, tree, uuid, extend_all_attributes, expand)
-
-    if datatype_tree is not None:
-        data_types_yaml_dict: Dict[str, Any] = {}
-        export_node(
-            data_types_yaml_dict, datatype_tree, uuid, extend_all_attributes, expand
-        )
+    if datatype_tree:
+        datatype_tree_data = datatype_tree.as_flat_dict(extend_all_attributes)
         if not types_output:
             log.info("Adding custom data types to signal dictionary")
-            signals_yaml_dict["ComplexDataTypes"] = data_types_yaml_dict
+            tree_data["ComplexDataTypes"] = datatype_tree_data
         else:
-            export_yaml(types_output, data_types_yaml_dict)
+            export_yaml(types_output, datatype_tree_data)
 
-    export_yaml(output, signals_yaml_dict)
+    export_yaml(output, tree_data)


Test patch:
diff --git a/tests/backward_compatibility/test_compatibility.py b/tests/backward_compatibility/test_compatibility.py
index bc1f7661..b0ba40a6 100644
--- a/tests/backward_compatibility/test_compatibility.py
+++ b/tests/backward_compatibility/test_compatibility.py
@@ -26,12 +26,9 @@
 #
 
 
-@pytest.mark.parametrize("tag",
-                         [
-                          'v4',
-                          'v4.0',
-                          'v4.1',
-                          'v4.2'])
+# TODO: Should be reparametrized to future releases
+@pytest.mark.skip(reason="Strict unit and quantity requires old releases to be patched")
+@pytest.mark.parametrize("tag", ["v4", "v4.0", "v4.1", "v4.2"])
 def test_compatibility(tag, tmp_path):
     """
     Test that we still can analyze wanted versions without error
diff --git a/tests/binary/test_binary.py b/tests/binary/test_binary.py
index cf989e71..e02e19eb 100644
--- a/tests/binary/test_binary.py
+++ b/tests/binary/test_binary.py
@@ -11,6 +11,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "vspec" / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "vspec" / "test_quantities.yaml"
 BIN_DIR = HERE / ".." / ".." / "binary"
 
 
@@ -34,11 +35,10 @@ def test_binary(tmp_path):
     ctestparser = tmp_path / "ctestparser"
     gotestparser = tmp_path / "gotestparser"
     bintool_lib = tmp_path / "binarytool.so"
-    cmd = (
-        f"gcc -shared -o {bintool_lib} -fPIC {BIN_DIR / 'binarytool.c'}"
-    )
+    cmd = f"gcc -shared -o {bintool_lib} -fPIC {BIN_DIR / 'binarytool.c'}"
     subprocess.run(cmd.split(), check=True)
-    cmd = f"vspec export binary -b {bintool_lib} -u {TEST_UNITS} -s {HERE / 'test.vspec'} -o {test_binary}"
+    cmd = f"vspec export binary -b {bintool_lib} -u {TEST_UNITS}"
+    cmd += f" -q {TEST_QUANT} -s {HERE / 'test.vspec'} -o {test_binary}"
     subprocess.run(cmd.split(), check=True)
     cmd = f"cc {BIN_DIR / 'c_parser/testparser.c'} {BIN_DIR / 'c_parser/cparserlib.c'} -o {ctestparser}"
     subprocess.run(cmd.split(), check=True)
@@ -47,7 +47,5 @@ def test_binary(tmp_path):
 
     parsers = [ctestparser, gotestparser]
     for parser in parsers:
-        check_expected_for_tool(
-            parser, "A.String", "Node type=SENSOR", test_binary)
-        check_expected_for_tool(
-            parser, "A.Int", "Node type=ACTUATOR", test_binary)
+        check_expected_for_tool(parser, "A.String", "Node type=SENSOR", test_binary)
+        check_expected_for_tool(parser, "A.Int", "Node type=ACTUATOR", test_binary)
diff --git a/tests/instances/test_instances.py b/tests/instances/test_instances.py
index d6f13626..2d607046 100644
--- a/tests/instances/test_instances.py
+++ b/tests/instances/test_instances.py
@@ -6,11 +6,11 @@
 #
 # SPDX-License-Identifier: MPL-2.0
 
-from typing import Dict
-from vss_tools.vspec.model.vsstree import VSSNode
-from vss_tools.vspec.model.constants import VSSType, VSSTreeType
-import vss_tools.vspec as vspec
+from anytree import RenderTree
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.main import get_trees
 from pathlib import Path
+import vss_tools.vspec.model as model
 import re
 
 HERE = Path(__file__).resolve().parent
@@ -38,24 +38,25 @@
 def test_simple_structures(request):
     for tfs in TEST_FILES_SIMPLE:
         # load the file
-        tree = vspec.load_tree(
-            str(RESOURCES / tfs), str(RESOURCES), VSSTreeType.SIGNAL_TREE
-        )
+        print(tfs)
+        tree, _ = get_trees(vspec=RESOURCES / tfs, include_dirs=(RESOURCES,))
 
+        print(RenderTree(tree).by_attr())
         # check if root node has 3 children
         assert len(tree.children) == 3
 
         list_of_instances = ["Vehicle.Test1", "Vehicle.Test2", "Vehicle.Test3"]
 
+        child: VSSNode
         for child in tree.children:
-            assert child.qualified_name() in list_of_instances  # < list qualified names
-            assert child.type == VSSType.BRANCH
-            assert child.description == "High-level vehicle data."
+            assert child.get_fqn() in list_of_instances  # < list qualified names
+            assert child.data.type == model.NodeType.BRANCH
+            assert child.data.description == "High-level vehicle data."
             # < check that they have exactly one child, which is named...
             assert len(child.children) == 1
             assert child.children[0].name == "SomeThing"  # <... SomeThing
-            assert child.children[0].type == VSSType.SENSOR
-            assert child.children[0].description == "test"
+            assert child.children[0].data.type == model.NodeType.SENSOR
+            assert child.children[0].data.description == "test"
 
 
 # TEST COMPLEX INSTANCE STRUCTURE #
@@ -82,42 +83,43 @@ def test_complex_structures():
     # helper function to check, if all information are present
 
     def check_instance_branch(branch: VSSNode, numberOfChildren: int):
-        assert branch.type == VSSType.BRANCH
-        assert branch.description == "High-level vehicle data."
+        assert branch.data.type == model.NodeType.BRANCH
+        assert branch.data.description == "High-level vehicle data."
 
     for tfs in TEST_FILES_COMPLEX:
+        print(tfs)
         # load the file
-        tree = vspec.load_tree(
-            str(RESOURCES / tfs), str(RESOURCES), VSSTreeType.SIGNAL_TREE
-        )
+        tree, _ = get_trees(vspec=RESOURCES / tfs, include_dirs=(RESOURCES,))
 
         # check if root node has 1 child and check first instances
         assert len(tree.children) == 1
-        assert tree.children[0].qualified_name() == "Vehicle.Test1"
+        assert tree.children[0].get_fqn() == "Vehicle.Test1"
         check_instance_branch(tree.children[0], 2)
 
         # go through the instances and check if expected structure maps
+        child: VSSNode
         for child in tree.children[0].children:
             # 2nd level: Test1.Test2 - Test1.Test3
-            assert child.qualified_name() in [
+            assert child.get_fqn() in [
                 "Vehicle.Test1.Test2",
                 "Vehicle.Test1.Test3",
             ]
             check_instance_branch(child, 3)
+            child_2: VSSNode
             for child_2 in child.children:
                 # 2nd level: Test1.Test2.Test4 - Test1.Test3.Test6
-                print(child_2.qualified_name())
+                print(child_2.get_fqn())
                 assert None is not re.match(
-                    "^Vehicle.Test1.Test(2|3).Test(4|5|6)", child_2.qualified_name(
-                    )
+                    "^Vehicle.Test1.Test(2|3).Test(4|5|6)", child_2.get_fqn()
                 )
                 check_instance_branch(child_2, 4)
+                child_3: VSSNode
                 for child_3 in child_2.children:
                     # 3rd level: Test1.Test2.Test4.Test7 -
                     # Test1.Test3.Test6.Test10
                     assert None is not re.match(
                         "^Vehicle.Test1.Test(2|3).Test(4|5|6).Test(7|8|9|10)",
-                        child_3.qualified_name(),
+                        child_3.get_fqn(),
                     )
                     check_instance_branch(child_3, 1)
                     assert 1 == len(child_3.children)
@@ -126,13 +128,13 @@ def check_instance_branch(branch: VSSNode, numberOfChildren: int):
                     # Test1.Test3.Test6.Test10.Test11
                     assert None is not re.match(
                         "^Vehicle.Test1.Test(2|3).Test(4|5|6).Test(7|8|9|10).Test11",
-                        child_4.qualified_name(),
+                        child_4.get_fqn(),
                     )
                     # All instances are expected to have one child
                     assert 1 == len(child_4.children)
                     assert child_4.children[0].name == "SomeThing"
-                    assert child_4.children[0].type == VSSType.SENSOR
-                    assert child_4.children[0].description == "test"
+                    assert child_4.children[0].data.type == model.NodeType.SENSOR
+                    assert child_4.children[0].data.description == "test"
 
 
 # TEST EXCLUSION FROM INSTANCE STRUCTURE #
@@ -150,40 +152,36 @@ def check_instance_branch(branch: VSSNode, numberOfChildren: int):
 def test_exclusion_from_instance():
     for tfs in TEST_FILES_EXCLUDE:
         # load the file
-        tree = vspec.load_tree(
-            str(RESOURCES / tfs), str(RESOURCES), VSSTreeType.SIGNAL_TREE
-        )
+        print(tfs)
+        tree, _ = get_trees(vspec=RESOURCES / tfs, include_dirs=(RESOURCES,))
         assert 6 == len(tree.children)
         name_list = []
+        child: VSSNode
         for child in tree.children:
-            name_list.append(child.qualified_name())
-            if "Vehicle.ExcludeSomeThing" == child.qualified_name():
-                assert VSSType.BRANCH == child.type
-                assert "ExcludeSomeThing description" == child.description
+            name_list.append(child.get_fqn())
+            if "Vehicle.ExcludeSomeThing" == child.get_fqn():
+                assert model.NodeType.BRANCH == child.data.type
+                assert "ExcludeSomeThing description" == child.data.description
                 assert 1 == len(child.children)
                 child_2 = child.children[0]
                 assert (
-                    "Vehicle.ExcludeSomeThing.ExcludeSomethingLeaf"
-                    == child_2.qualified_name()
+                    "Vehicle.ExcludeSomeThing.ExcludeSomethingLeaf" == child_2.get_fqn()
                 )
-                assert "ExcludeSomethingLeaf description" == child_2.description
-                assert VSSType.ACTUATOR == child_2.type
+                assert "ExcludeSomethingLeaf description" == child_2.data.description
+                assert model.NodeType.ACTUATOR == child_2.data.type
 
-            elif "Vehicle.ExcludeNode" == child.qualified_name():
-                assert "Vehicle.ExcludeNode" == child.qualified_name()
-                assert "ExcludeNode description" == child.description
-                assert VSSType.ATTRIBUTE == child.type
+            elif "Vehicle.ExcludeNode" == child.get_fqn():
+                assert "Vehicle.ExcludeNode" == child.get_fqn()
+                assert "ExcludeNode description" == child.data.description
+                assert model.NodeType.ATTRIBUTE == child.data.type
 
         assert "Vehicle.ExcludeSomeThing" in name_list
         assert "Vehicle.ExcludeNode" in name_list
 
 
 def test_extended_attribute():
-    # load the file
-    tree = vspec.load_tree(
-        str(RESOURCES / "instance_extended_attribute.vspec"),
-        str(RESOURCES),
-        VSSTreeType.SIGNAL_TREE,
+    tree, _ = get_trees(
+        vspec=RESOURCES / "instance_extended_attribute.vspec", include_dirs=(RESOURCES,)
     )
 
     # check if root node has 3 children
@@ -191,22 +189,20 @@ def test_extended_attribute():
 
     # Programmatically change value of dbc/signal for instance in Test2
 
+    child: VSSNode
     for child in tree.children:
-        if child.qualified_name() == "Vehicle.Test2":
+        if child.get_fqn() == "Vehicle.Test2":
             assert len(child.children) == 1
             assert child.children[0].name == "SomeThing"  # <... SomeThing
-            assert len(child.children[0].extended_attributes.items()) == 1
-            assert isinstance(
-                child.children[0].extended_attributes["dbc"], Dict)
-            assert isinstance(
-                child.children[0].extended_attributes["dbc"]["signal"], str
-            )
-            assert child.children[0].extended_attributes["dbc"]["signal"] == "bababa"
-            child.children[0].extended_attributes["dbc"]["signal"] = "lalala"
+            assert len(child.children[0].data.get_extra_attributes()) == 1
+            assert isinstance(getattr(child.children[0].data, "dbc"), dict)
+            assert getattr(child.children[0].data, "dbc")["signal"] == "bababa"
+            child.children[0].data.dbc["signal"] = "lalala"
 
+    child: VSSNode
     for child in tree.children:
-        if child.qualified_name() == "Vehicle.Test2":
-            assert child.children[0].extended_attributes["dbc"]["signal"] == "lalala"
+        if child.get_fqn() == "Vehicle.Test2":
+            assert child.children[0].data.dbc["signal"] == "lalala"
         else:
             # Make sure that all other instances keeps same name
-            assert child.children[0].extended_attributes["dbc"]["signal"] == "bababa"
+            assert child.children[0].data.dbc["signal"] == "bababa"
diff --git a/tests/model/explicit_units.yaml b/tests/model/explicit_units.yaml
deleted file mode 100644
index fdfd0a23..00000000
--- a/tests/model/explicit_units.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-puncheon:
-  definition: Volume measure in puncheons (1 puncheon = 318 liters)
-  unit: Puncheon
-  quantity: volume
-  allowed_datatypes: ['uint16', 'int16']
-hogshead:
-  definition: Volume measure in hogsheads (1 hogshead = 238 liters)
-  unit: Hogshead
-  quantity: volume
-  allowed_datatypes: ['numeric']
diff --git a/tests/model/explicit_units_old_syntax.yaml b/tests/model/explicit_units_old_syntax.yaml
deleted file mode 100644
index d3d8b441..00000000
--- a/tests/model/explicit_units_old_syntax.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-units:
-  puncheon:
-    label: Puncheon
-    description: Volume measure in puncheons (1 puncheon = 318 liters)
-    domain: volume
-  hogshead:
-    label: Hogshead
-    description: Volume measure in hogsheads (1 hogshead = 238 liters)
-    domain: volume
diff --git a/tests/model/test_contants.py b/tests/model/test_contants.py
deleted file mode 100644
index 125410f9..00000000
--- a/tests/model/test_contants.py
+++ /dev/null
@@ -1,120 +0,0 @@
-# Copyright (c) 2020 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-import pytest
-import os
-
-from vss_tools.vspec.model.constants import VSSType, VSSDataType, VSSUnitCollection, VSSTreeType
-from vss_tools.vspec.model.constants import VSSUnit, VSSQuantity
-
-
-@pytest.mark.parametrize("unit_file",
-                         ['explicit_units.yaml',
-                          'explicit_units_old_syntax.yaml'])
-def test_manually_loaded_units(unit_file):
-    """
-    Test correct parsing of units
-    """
-    unit_file = os.path.join(os.path.dirname(__file__), unit_file)
-    VSSUnitCollection.load_config_file(unit_file)
-    assert VSSUnitCollection.get_unit("puncheon") == "puncheon"
-    assert VSSUnitCollection.get_unit("puncheon").definition == \
-           "Volume measure in puncheons (1 puncheon = 318 liters)"
-    assert VSSUnitCollection.get_unit("puncheon").unit == \
-           "Puncheon"
-    assert VSSUnitCollection.get_unit("puncheon").quantity == \
-           "volume"
-
-
-def test_invalid_unit():
-    assert VSSUnitCollection.get_unit("unknown") is None
-
-
-@pytest.mark.parametrize("type_enum,type_str",
-                         [(VSSType.BRANCH, "branch"),
-                          (VSSType.ATTRIBUTE, "attribute"),
-                          (VSSType.SENSOR, "sensor"),
-                          (VSSType.ACTUATOR, "actuator"),
-                          (VSSType.PROPERTY, "property")])
-def test_vss_types(type_enum, type_str, ):
-    """
-    Test correct parsing of VSS Types
-    """
-
-    assert type_enum == VSSType.from_str(type_str)
-
-
-def test_invalid_vss_types():
-    with pytest.raises(Exception):
-        VSSType.from_str("not_a_valid_case")
-
-
-@pytest.mark.parametrize("data_type_enum,data_type_str",
-                         [(VSSDataType.INT8, "int8"),
-                          (VSSDataType.UINT8, "uint8"),
-                          (VSSDataType.INT16, "int16"),
-                          (VSSDataType.UINT16, "uint16"),
-                          (VSSDataType.INT32, "int32"),
-                          (VSSDataType.UINT32, "uint32"),
-                          (VSSDataType.INT64, "int64"),
-                          (VSSDataType.UINT64, "uint64"),
-                          (VSSDataType.BOOLEAN, "boolean"),
-                          (VSSDataType.FLOAT, "float"),
-                          (VSSDataType.DOUBLE, "double"),
-                          (VSSDataType.STRING, "string")])
-def test_vss_data_types(data_type_enum, data_type_str, ):
-    """
-    Test correct parsing of VSS Data Types
-    """
-    assert data_type_enum == VSSDataType.from_str(data_type_str)
-
-
-def test_invalid_vss_data_types():
-    with pytest.raises(Exception):
-        VSSDataType.from_str("not_a_valid_case")
-
-
-@pytest.mark.parametrize("tree_type_enum,tree_type_str, important_types",
-                         [(VSSTreeType.SIGNAL_TREE, "signal_tree", ["sensor", "actuator", "attribute", "branch"]),
-                          (VSSTreeType.DATA_TYPE_TREE, "data_type_tree", ["struct", "property", "branch"])])
-def test_vss_tree_types(tree_type_enum, tree_type_str, important_types):
-    """
-    Test correct parsing of VSS Tree Types
-    """
-
-    assert tree_type_enum == VSSTreeType.from_str(tree_type_str)
-    available_types = tree_type_enum.available_types()
-    for t in important_types:
-        assert t in available_types
-
-
-def test_invalid_vss_tree_types():
-    with pytest.raises(Exception):
-        VSSDataType.from_str("not_a_valid_case")
-
-
-def test_unit():
-    """ Test Unit class """
-    item = VSSUnit("myid", "myunit", "mydefinition", "myquantity")
-    assert item.value == "myid"
-    assert item.unit == "myunit"
-    assert item.definition == "mydefinition"
-    assert item.quantity == "myquantity"
-    # String subclass so just comparing shall get "myid"
-    assert item == "myid"
-
-
-def test_quantity():
-    """ Test Quantity class """
-    item = VSSQuantity("myid", "mydefinition", "myremark", "mycomment")
-    assert item.value == "myid"
-    assert item.definition == "mydefinition"
-    assert item.remark == "myremark"
-    assert item.comment == "mycomment"
-    # String subclass so just comparing shall get "myid"
-    assert item == "myid"
diff --git a/tests/model/test_vsstree.py b/tests/model/test_vsstree.py
deleted file mode 100644
index 119b0355..00000000
--- a/tests/model/test_vsstree.py
+++ /dev/null
@@ -1,324 +0,0 @@
-# Copyright (c) 2020 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-import unittest
-import os
-import pytest
-
-from vss_tools.vspec.model.constants import VSSType, VSSDataType, VSSUnitCollection, VSSTreeType
-from vss_tools.vspec.model.vsstree import VSSNode
-from vss_tools.vspec.model.exceptions import NameStyleValidationException
-
-
-class TestVSSNode(unittest.TestCase):
-    def test_simple_construction(self):
-        """
-        Test minimal object construction.
-        """
-        source = {
-            "description": "some desc",
-            "type": "branch",
-            "uuid": "26d6e362-a422-11ea-bb37-0242ac130002",
-            "$file_name$": "testfile"}
-        node = VSSNode(
-            "test",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        self.assertIsNotNone(node)
-        self.assertEqual("some desc", node.description)
-        self.assertEqual(VSSType.BRANCH, node.type)
-        self.assertEqual("26d6e362-a422-11ea-bb37-0242ac130002", node.uuid)
-        self.assertFalse(node.has_unit())
-        self.assertFalse(node.has_datatype())
-
-    def test_complex_construction(self):
-        """
-        Test complex object construction.
-        """
-        source = {"description": "some desc", "type": "sensor", "uuid": "26d6e362-a422-11ea-bb37-0242ac130002",
-                  "datatype": "uint8", "unit": "hogshead", "min": 0, "max": 100, "allowed": ["one", "two"],
-                  "aggregate": False,
-                  "default": "test-default", "$file_name$": "testfile"}
-        unit_file = os.path.join(os.path.dirname(__file__), 'explicit_units.yaml')
-        VSSUnitCollection.load_config_file(unit_file)
-        node = VSSNode(
-            "test",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        self.assertIsNotNone(node)
-        self.assertEqual("some desc", node.description)
-        self.assertEqual(VSSType.SENSOR, node.type)
-        self.assertEqual("26d6e362-a422-11ea-bb37-0242ac130002", node.uuid)
-        self.assertEqual(VSSDataType.UINT8, node.datatype)
-        self.assertEqual(VSSUnitCollection.get_unit("hogshead"), node.unit)
-        self.assertEqual(0, node.min)
-        self.assertEqual(100, node.max)
-        self.assertEqual(["one", "two"], node.allowed)
-        self.assertEqual(False, node.aggregate)
-        self.assertEqual("test-default", node.default)
-        self.assertTrue(node.has_unit())
-        self.assertTrue(node.has_datatype())
-
-    def test_merge_nodes(self):
-        """
-        Tests if merging two nodes works as expected
-        """
-
-        target = {
-            "description": "some desc",
-            "type": "sensor",
-            "datatype": "float",
-            "uuid": "e36a1d8c-4d06-4c22-ba69-e8b39434a7a3",
-            "$file_name$": "testfile"}
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "uint8", "unit": "hogshead", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        unit_file = os.path.join(os.path.dirname(__file__), 'explicit_units.yaml')
-        VSSUnitCollection.reset_units()
-        VSSUnitCollection.load_config_file(unit_file)
-
-        node_target = VSSNode(
-            "MyNode",
-            target,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        node_source = VSSNode(
-            "MyNode2",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        self.assertEqual(
-            "e36a1d8c-4d06-4c22-ba69-e8b39434a7a3",
-            node_target.uuid)
-        self.assertTrue(node_target.has_datatype())
-        self.assertEqual(
-            "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-            node_source.uuid)
-
-        node_target.merge(node_source)
-        self.assertEqual(
-            "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-            node_target.uuid)
-        self.assertTrue(node_target.has_datatype())
-        self.assertEqual(VSSDataType.UINT8, node_target.datatype)
-        self.assertEqual(VSSUnitCollection.get_unit("hogshead"), node_target.unit)
-        self.assertEqual(0, node_target.min)
-        self.assertEqual(100, node_target.max)
-
-    def test_unit_datatype(self):
-
-        unit_file = os.path.join(os.path.dirname(__file__), 'explicit_units.yaml')
-        VSSUnitCollection.reset_units()
-        VSSUnitCollection.load_config_file(unit_file)
-
-        # int16 explicitly listed
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "int16", "unit": "puncheon", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        node_source = VSSNode(
-            "MyNode2",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        assert node_source.unit == VSSUnitCollection.get_unit("puncheon")
-
-        # uint16 explicitly listed
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "uint16", "unit": "puncheon", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        node_source = VSSNode(
-            "MyNode2",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        assert node_source.unit == VSSUnitCollection.get_unit("puncheon")
-
-        # uint16[] is ok as uint16 is explicitly listed
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "uint16[]", "unit": "puncheon", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        node_source = VSSNode(
-            "MyNode2",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        assert node_source.unit == VSSUnitCollection.get_unit("puncheon")
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "float", "unit": "puncheon", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        # float not ok
-
-        with pytest.raises(SystemExit):
-            node_source = VSSNode(
-                "MyNode2",
-                source,
-                VSSTreeType.SIGNAL_TREE.available_types())
-
-        # Hogshead defined as "numeric", i.e. all numeric types shall be accepted
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "uint16", "unit": "hogshead", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        node_source = VSSNode(
-            "MyNode2",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        assert node_source.unit == VSSUnitCollection.get_unit("hogshead")
-
-        # Also array of numeric
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "uint16[]", "unit": "hogshead", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        node_source = VSSNode(
-            "MyNode2",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        assert node_source.unit == VSSUnitCollection.get_unit("hogshead")
-
-        # But not string
-
-        source = {"description": "some desc", "type": "sensor",
-                  "uuid": "2cc90035-e1c2-43bf-a394-1a439addc8ad",
-                  "datatype": "string", "unit": "hogshead", "min": 0, "max": 100, "$file_name$": "testfile"}
-
-        with pytest.raises(SystemExit):
-            node_source = VSSNode(
-                "MyNode2",
-                source,
-                VSSTreeType.SIGNAL_TREE.available_types())
-
-    def test_tree_find(self):
-        """
-        Test if tree can be found within VSS tree
-        """
-        source_root = {
-            "description": "High-level vehicle data.",
-            "type": "branch",
-            "uuid": "f6750f15-ba2f-4eab-adcc-19a500982293",
-            "$file_name$": "testfile"}
-        source_drivetrain = {
-            "description": "Drivetrain branch.",
-            "type": "branch",
-            "uuid": "5fb3f710-ed60-426b-a083-015cc7c7bc1b",
-            "$file_name$": "testfile"}
-        source_drivetrain_element = {
-            "description": "Some Drivetrain element.",
-            "type": "sensor",
-            "datatype": "string",
-            "uuid": "9101bbea-aeb6-4b0e-8cec-d8b126e77898",
-            "$file_name$": "testfile"}
-        node_root = VSSNode(
-            "Vehicle",
-            source_root,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        node_drivetrain = VSSNode(
-            "Drivetrain",
-            source_drivetrain,
-            VSSTreeType.SIGNAL_TREE.available_types(),
-            node_root)
-        # No need to store create node to a variable on next line
-        VSSNode(
-            "SomeElement",
-            source_drivetrain_element,
-            VSSTreeType.SIGNAL_TREE.available_types(),
-            node_drivetrain)
-
-        self.assertTrue(
-            VSSNode.node_exists(
-                node_root,
-                "/Vehicle/Drivetrain/SomeElement"))
-
-    def test_orphan_detection(self):
-        """
-        Test if vssnode finds orphan (r)branches.
-        """
-        source_root = {
-            "description": "High-level vehicle data.",
-            "type": "branch",
-            "uuid": "f6750f15-ba2f-4eab-adcc-19a500982293",
-            "$file_name$": "testfile"}
-        source_drivetrain = {
-            "description": "Drivetrain branch.",
-            "type": "branch",
-            "uuid": "5fb3f710-ed60-426b-a083-015cc7c7bc1b",
-            "$file_name$": "testfile"}
-        node_root = VSSNode(
-            "Vehicle",
-            source_root,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        node_drivetrain = VSSNode(
-            "Drivetrain",
-            source_drivetrain,
-            VSSTreeType.SIGNAL_TREE.available_types(),
-            node_root)
-
-        self.assertTrue(node_drivetrain.is_orphan())
-
-    def test_name_style_string(self):
-
-        source = {
-            "description": "Some element.",
-            "type": "sensor",
-            "datatype": "string",
-            "$file_name$": "testfile"}
-
-        node = VSSNode(
-            "this_name_is_not_allowed_in_standard_catalog",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        with pytest.raises(NameStyleValidationException):
-            node.validate_name_style("dummyfile")
-
-        node = VSSNode(
-            "ButThisNameIs",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        # No exception expected
-        node.validate_name_style("dummyfile")
-
-    def test_name_style_boolean(self):
-
-        source = {
-            "description": "Some element.",
-            "type": "sensor",
-            "datatype": "boolean",
-            "$file_name$": "testfile"}
-
-        node = VSSNode(
-            "this_name_is_not_allowed_in_standard_catalog",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        with pytest.raises(NameStyleValidationException):
-            node.validate_name_style("dummyfile")
-
-        # Boolean ones must start with "Is"
-        node = VSSNode(
-            "ThisNameNeither",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        with pytest.raises(NameStyleValidationException):
-            node.validate_name_style("dummyfile")
-
-        node = VSSNode(
-            "IsThisAllowedYesItIs",
-            source,
-            VSSTreeType.SIGNAL_TREE.available_types())
-        # No exception expected
-        node.validate_name_style("dummyfile")
-
-
-if __name__ == '__main__':
-    unittest.main()
diff --git a/tests/model/test_vsstree_default.py b/tests/model/test_vsstree_default.py
deleted file mode 100644
index 4c61a32a..00000000
--- a/tests/model/test_vsstree_default.py
+++ /dev/null
@@ -1,70 +0,0 @@
-# Copyright (c) 2023 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-import pytest
-
-from vss_tools.vspec.model.constants import VSSTreeType
-from vss_tools.vspec.model.vsstree import VSSNode
-
-
-@pytest.mark.parametrize("type_name, is_signal_tree, is_default_supported", [
-    ("attribute", True, True),
-    ("actuator", True, True),
-    ("sensor", True, True),
-    ("branch", True, False),
-    ("branch", False, False),
-    ("struct", False, False),
-    ("property", False, True)
-    ])
-def test_default(type_name: str, is_signal_tree: bool, is_default_supported: bool):
-    """
-    Verify that tooling complains if "default" is used where it should not be used
-    """
-    source = {
-                "description": "some desc",
-                "type": type_name,
-                "$file_name$": "testfile",
-                "default": 1234}
-
-    if type_name in ["attribute", "sensor", "actuator", "property"]:
-        source["datatype"] = "uint8"
-
-    if is_signal_tree:
-        type_set = VSSTreeType.SIGNAL_TREE.available_types()
-    else:
-        type_set = VSSTreeType.DATA_TYPE_TREE.available_types()
-
-    if type_name in ["property"]:
-        # Create a struct above
-        source_struct = {
-                "description": "some desc",
-                "type": "struct",
-                "$file_name$": "testfile"}
-
-        parent = VSSNode(
-                "TestStruct",
-                source_struct,
-                type_set)
-    else:
-        parent = None
-
-    node = VSSNode(
-                "Test",
-                source,
-                type_set,
-                parent=parent)
-
-    assert node is not None
-
-    if is_default_supported:
-        node.verify_attributes(True)
-    else:
-        with pytest.raises(SystemExit) as pytest_wrapped_e:
-            node.verify_attributes(True)
-        assert pytest_wrapped_e.type == SystemExit
-        assert pytest_wrapped_e.value.code == -1
diff --git a/tests/vspec/test_allowed/expected.csv b/tests/vspec/test_allowed/expected.csv
index 6a5d1c4e..1c3a505d 100644
--- a/tests/vspec/test_allowed/expected.csv
+++ b/tests/vspec/test_allowed/expected.csv
@@ -1,6 +1,6 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.String","sensor","string","","","","","A string","","['January', 'February']",""
-"A.StringArray","sensor","string[]","","","","","A string array","","['January', 'February', 'March']",""
-"A.Int","sensor","uint16","","","","","An int","","[1, 2, 3]",""
-"A.Float","actuator","float","","","","","A float","","[1.1, 2.54, 3]",""
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.String,sensor,string,,,,,A string,,"['January', 'February']",
+A.StringArray,sensor,string[],,,,,A string array,,"['January', 'February', 'March']",
+A.Int,sensor,uint16,,,,,An int,,"[1, 2, 3]",
+A.Float,actuator,float,,,,,A float,,"[1.1, 2.54, 3]",
diff --git a/tests/vspec/test_allowed/test_allowed.py b/tests/vspec/test_allowed/test_allowed.py
index 90c06074..6bab4624 100644
--- a/tests/vspec/test_allowed/test_allowed.py
+++ b/tests/vspec/test_allowed/test_allowed.py
@@ -10,8 +10,10 @@
 import subprocess
 import filecmp
 
+
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def run_exporter(exporter, argument, tmp_path):
@@ -19,7 +21,8 @@ def run_exporter(exporter, argument, tmp_path):
     output = tmp_path / f"out.{exporter}"
     cmd = f"vspec export {exporter}{argument} --vspec {spec} --output {output}"
 
-    process = subprocess.run(cmd.split(), check=True, capture_output=True, text=True)
+    process = subprocess.run(cmd.split(), capture_output=True, text=True)
+    assert process.returncode == 0
     expected = HERE / f"expected.{exporter}"
     assert filecmp.cmp(output, expected)
 
@@ -35,4 +38,4 @@ def test_allowed(tmp_path):
     # Exception is "binary", as it is assumed output may vary depending on target
     exporters = ["json", "ddsidl", "csv", "yaml", "franca", "graphql"]
     for exporter in exporters:
-        run_exporter(exporter, f" -u {TEST_UNITS}", tmp_path)
+        run_exporter(exporter, f" -u {TEST_UNITS} -q {TEST_QUANT}", tmp_path)
diff --git a/tests/vspec/test_comment/expected.csv b/tests/vspec/test_comment/expected.csv
index 828e264c..03b962b8 100644
--- a/tests/vspec/test_comment/expected.csv
+++ b/tests/vspec/test_comment/expected.csv
@@ -1,16 +1,16 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.SingleLineNotQuoted","sensor","float","","km","","","A sensor.","January is a great month!","",""
-"A.SingleLineInternalQuotes","sensor","float","","km","","","A sensor.","February is a ""great"" month!","",""
-"A.SingleLineQuoted","sensor","float","","km","","","A sensor.","March is a great month!","",""
-"A.SingleLineQuotedInternalQuotes","sensor","float","","km","","","A sensor.","April is a ""great"" month!","",""
-"A.SingleLineComma","sensor","float","","km","","","A sensor.","May, is a great month!","",""
-"A.SingleLineCommaQuoted","sensor","float","","km","","","A sensor.","June, is a great month!","",""
-"A.MultiLineCommaNotQuoted","sensor","float","","km","","","A sensor.","July, is a great month!","",""
-"A.MultiLineCommaQuoted","sensor","float","","km","","","A sensor.","August, is a great month!","",""
-"A.MultiLineStyleInitialBreak","sensor","float","","km","","","A sensor.","October,
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.SingleLineNotQuoted,sensor,float,,km,,,A sensor.,January is a great month!,,
+A.SingleLineInternalQuotes,sensor,float,,km,,,A sensor.,"February is a ""great"" month!",,
+A.SingleLineQuoted,sensor,float,,km,,,A sensor.,March is a great month!,,
+A.SingleLineQuotedInternalQuotes,sensor,float,,km,,,A sensor.,"April is a ""great"" month!",,
+A.SingleLineComma,sensor,float,,km,,,A sensor.,"May, is a great month!",,
+A.SingleLineCommaQuoted,sensor,float,,km,,,A sensor.,"June, is a great month!",,
+A.MultiLineCommaNotQuoted,sensor,float,,km,,,A sensor.,"July, is a great month!",,
+A.MultiLineCommaQuoted,sensor,float,,km,,,A sensor.,"August, is a great month!",,
+A.MultiLineStyleInitialBreak,sensor,float,,km,,,A sensor.,"October,
 is a great month!
-","",""
-"A.MultiLineLiteralStyleQuote","sensor","float","","km","","","A sensor.","November, 
+",,
+A.MultiLineLiteralStyleQuote,sensor,float,,km,,,A sensor.,"November, 
 is a ""great"" month!
-","",""
+",,
diff --git a/tests/vspec/test_consistency/data/q_1.yaml b/tests/vspec/test_consistency/data/q_1.yaml
new file mode 100644
index 00000000..df428f70
--- /dev/null
+++ b/tests/vspec/test_consistency/data/q_1.yaml
@@ -0,0 +1,2 @@
+work:
+  definition: Vector quantity giving the rate of change of a position vector (ISO 80000-3:2019)
diff --git a/tests/vspec/test_consistency/data/s_1.yaml b/tests/vspec/test_consistency/data/s_1.yaml
new file mode 100644
index 00000000..14d33acc
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_1.yaml
@@ -0,0 +1,3 @@
+Vehicle:
+  type: branch
+  description: Vehicle
diff --git a/tests/vspec/test_consistency/data/s_2.yaml b/tests/vspec/test_consistency/data/s_2.yaml
new file mode 100644
index 00000000..ab900f67
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_2.yaml
@@ -0,0 +1,9 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Test:
+  type: sensor
+  datatype: uint8
+  arraysize: 2
+  description: Test
diff --git a/tests/vspec/test_consistency/data/s_3.yaml b/tests/vspec/test_consistency/data/s_3.yaml
new file mode 100644
index 00000000..ff4caa32
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_3.yaml
@@ -0,0 +1,13 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Test:
+  type: sensor
+  datatype: uint8[]
+  arraysize: 2
+  description: Test
+  allowed:
+  - 1
+  - 2
+  - foo
diff --git a/tests/vspec/test_consistency/data/s_4.yaml b/tests/vspec/test_consistency/data/s_4.yaml
new file mode 100644
index 00000000..785f4868
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_4.yaml
@@ -0,0 +1,12 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Test:
+  type: sensor
+  datatype: uint8
+  description: Test
+  min: 0
+  max: 3
+  allowed:
+  - 2
diff --git a/tests/vspec/test_consistency/data/s_5.yaml b/tests/vspec/test_consistency/data/s_5.yaml
new file mode 100644
index 00000000..ef4b54b9
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_5.yaml
@@ -0,0 +1,10 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Test:
+  type: sensor
+  datatype: uint8
+  description: Test
+  default: 4
+  allowed: [1,2,3]
diff --git a/tests/vspec/test_consistency/data/s_6.yaml b/tests/vspec/test_consistency/data/s_6.yaml
new file mode 100644
index 00000000..140b7d3d
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_6.yaml
@@ -0,0 +1,9 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Test:
+  type: sensor
+  datatype: string
+  description: Test
+  unit: kWh
diff --git a/tests/vspec/test_consistency/data/s_7.yaml b/tests/vspec/test_consistency/data/s_7.yaml
new file mode 100644
index 00000000..1c7c9777
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_7.yaml
@@ -0,0 +1,4 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+  datatype: uint8
diff --git a/tests/vspec/test_consistency/data/s_8.yaml b/tests/vspec/test_consistency/data/s_8.yaml
new file mode 100644
index 00000000..06ee9d94
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_8.yaml
@@ -0,0 +1,10 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Test:
+  type: attribute
+  datatype: uint8[]
+  default: [1,2,3]
+  arraysize: 2
+  description: Test
diff --git a/tests/vspec/test_consistency/data/s_9.yaml b/tests/vspec/test_consistency/data/s_9.yaml
new file mode 100644
index 00000000..a29e19d6
--- /dev/null
+++ b/tests/vspec/test_consistency/data/s_9.yaml
@@ -0,0 +1,9 @@
+Vehicle:
+  type: branch
+  description: Vehicle
+
+Vehicle.Test:
+  type: attribute
+  datatype: uint8[]
+  default: 3
+  description: Test
diff --git a/tests/vspec/test_consistency/data/u_1.yaml b/tests/vspec/test_consistency/data/u_1.yaml
new file mode 100644
index 00000000..dbd97f95
--- /dev/null
+++ b/tests/vspec/test_consistency/data/u_1.yaml
@@ -0,0 +1,5 @@
+kWh:
+  definition: Energy consumption measured in kilowatt hours
+  unit: kilowatt hours
+  quantity: work
+  allowed-datatypes: ['numerics']
diff --git a/tests/vspec/test_consistency/data/u_2.yaml b/tests/vspec/test_consistency/data/u_2.yaml
new file mode 100644
index 00000000..e00e59c1
--- /dev/null
+++ b/tests/vspec/test_consistency/data/u_2.yaml
@@ -0,0 +1,5 @@
+kWh:
+  definition: Energy consumption measured in kilowatt hours
+  unit: kilowatt hours
+  quantity: workie
+  allowed-datatypes: ['numeric']
diff --git a/tests/vspec/test_consistency/data/u_3.yaml b/tests/vspec/test_consistency/data/u_3.yaml
new file mode 100644
index 00000000..53afbbe5
--- /dev/null
+++ b/tests/vspec/test_consistency/data/u_3.yaml
@@ -0,0 +1,5 @@
+kWh:
+  description: Energy consumption measured in kilowatt hours
+  unit: kilowatt hours
+  quantity: work
+  allowed-datatypes: ['numeric']
diff --git a/tests/vspec/test_consistency/data/u_4.yaml b/tests/vspec/test_consistency/data/u_4.yaml
new file mode 100644
index 00000000..a5a0b01a
--- /dev/null
+++ b/tests/vspec/test_consistency/data/u_4.yaml
@@ -0,0 +1,5 @@
+kWh:
+  definition: Energy consumption measured in kilowatt hours
+  unit: kilowatt hours
+  quantity: work
+  allowed-datatypes: ['numeric']
diff --git a/tests/vspec/test_consistency/test_consistency.py b/tests/vspec/test_consistency/test_consistency.py
new file mode 100644
index 00000000..e1f46ab0
--- /dev/null
+++ b/tests/vspec/test_consistency/test_consistency.py
@@ -0,0 +1,41 @@
+# Copyright (c) 2023 Contributors to COVESA
+#
+# This program and the accompanying materials are made available under the
+# terms of the Mozilla Public License 2.0 which is available at
+# https://www.mozilla.org/en-US/MPL/2.0/
+#
+# SPDX-License-Identifier: MPL-2.0
+import pytest
+from pathlib import Path
+import subprocess
+
+HERE = Path(__file__).resolve().parent
+DATA = HERE / "data"
+
+
+@pytest.mark.parametrize(
+    "spec_id, quantity_id, unit_id, expected_out",
+    [
+        (1, 1, 1, "Invalid datatype: 'numerics'"),
+        (1, 1, 2, "Invalid quantity: 'workie'"),
+        (1, 1, 3, "Field required"),
+        (2, 1, 4, "'arraysize' set on a non array datatype"),
+        (3, 1, 4, "'foo' is not of type 'uint8[]'"),
+        (4, 1, 4, "'min/max' and 'allowed' cannot be used together"),
+        (5, 1, 4, "default value '4' is not in 'allowed' list"),
+        (6, 1, 4, "'string' is not allowed for unit 'kWh'"),
+        (7, 1, 4, "Forbidden extra attribute (core attribute): 'Vehicle':'datatype'"),
+        (8, 1, 4, "'default' array size does not match 'arraysize'"),
+    ],
+)
+def test_consistency(
+    spec_id: int, quantity_id: int, unit_id: int, expected_out: str, tmp_path: Path
+):
+    spec = DATA / f"s_{spec_id}.yaml"
+    quantity = DATA / f"q_{quantity_id}.yaml"
+    unit = DATA / f"u_{unit_id}.yaml"
+    log = tmp_path / "log.txt"
+    cmd = f"vspec --log-file {log} export tree -s {spec} -u {unit} -q {quantity}"
+    process = subprocess.run(cmd.split(), capture_output=True, text=True)
+    assert process.returncode != 0
+    assert expected_out in log.read_text()
diff --git a/tests/vspec/test_datatypes/expected.csv b/tests/vspec/test_datatypes/expected.csv
index 18281320..f74671fb 100644
--- a/tests/vspec/test_datatypes/expected.csv
+++ b/tests/vspec/test_datatypes/expected.csv
@@ -1,14 +1,13 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.UInt8","sensor","uint8","","km","","","A uint8.","","",""
-"A.Int8","sensor","int8","","km","","","An int8.","","",""
-"A.UInt16","sensor","uint16","","km","","","A uint16.","","",""
-"A.Int16","sensor","int16","","km","","","An int16.","","",""
-"A.UInt32","sensor","uint16","","km","","","A uint32.","","",""
-"A.Int32","sensor","int32","","km","","","An int32","","",""
-"A.UInt64","sensor","uint64","","km","","","A uint64.","","",""
-"A.Int64","sensor","int64","","km","","","An int64","","",""
-"A.IsBoolean","sensor","boolean","","","","","A boolean","","",""
-"A.Float","sensor","float","","km","","","A float.","","",""
-"A.Double","sensor","double","","km","","","A double.","","",""
-"A.String","sensor","string","","km","","","A string.","","",""
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.UInt8,sensor,uint8,,km,,,A uint8.,,,
+A.Int8,sensor,int8,,km,,,An int8.,,,
+A.UInt16,sensor,uint16,,km,,,A uint16.,,,
+A.Int16,sensor,int16,,km,,,An int16.,,,
+A.UInt32,sensor,uint16,,km,,,A uint32.,,,
+A.Int32,sensor,int32,,km,,,An int32,,,
+A.UInt64,sensor,uint64,,km,,,A uint64.,,,
+A.Int64,sensor,int64,,km,,,An int64,,,
+A.IsBoolean,sensor,boolean,,,,,A boolean,,,
+A.Float,sensor,float,,km,,,A float.,,,
+A.Double,sensor,double,,km,,,A double.,,,
diff --git a/tests/vspec/test_datatypes/expected.ddsidl b/tests/vspec/test_datatypes/expected.ddsidl
index 49af16e9..44e2479d 100644
--- a/tests/vspec/test_datatypes/expected.ddsidl
+++ b/tests/vspec/test_datatypes/expected.ddsidl
@@ -76,11 +76,4 @@ double value;
 //const string type ="sensor";
 //const string description="A double.";
 };
-struct _String
-{
-string value;
-//const string unit="km";
-//const string type ="sensor";
-//const string description="A string.";
-};
 };
diff --git a/tests/vspec/test_datatypes/expected.franca b/tests/vspec/test_datatypes/expected.franca
index 674b0b27..aa2d6997 100644
--- a/tests/vspec/test_datatypes/expected.franca
+++ b/tests/vspec/test_datatypes/expected.franca
@@ -83,11 +83,5 @@ const SignalSpec[] signal_spec = [
 	description: "A double.",
 	datatype: "double",
 	unit: "km"
-},
-{	name: "A.String",
-	type: "sensor",
-	description: "A string.",
-	datatype: "string",
-	unit: "km"
 }
 ]
\ No newline at end of file
diff --git a/tests/vspec/test_datatypes/expected.graphql b/tests/vspec/test_datatypes/expected.graphql
index 15570be0..d9891ead 100644
--- a/tests/vspec/test_datatypes/expected.graphql
+++ b/tests/vspec/test_datatypes/expected.graphql
@@ -44,9 +44,6 @@ type A {
 
   """A double."""
   double: A_Double
-
-  """A string."""
-  string: A_String
 }
 
 """A uint8."""
@@ -177,15 +174,3 @@ type A_Double {
   """Unit of A double."""
   unit: String
 }
-
-"""A string."""
-type A_String {
-  """Value: A string."""
-  value: String
-
-  """Timestamp: A string."""
-  timestamp: String
-
-  """Unit of A string."""
-  unit: String
-}
diff --git a/tests/vspec/test_datatypes/expected.json b/tests/vspec/test_datatypes/expected.json
index 862cf834..99a60a08 100644
--- a/tests/vspec/test_datatypes/expected.json
+++ b/tests/vspec/test_datatypes/expected.json
@@ -1,1 +1,1 @@
-{"A": {"children": {"Double": {"datatype": "double", "description": "A double.", "type": "sensor", "unit": "km"}, "Float": {"datatype": "float", "description": "A float.", "type": "sensor", "unit": "km"}, "Int16": {"datatype": "int16", "description": "An int16.", "type": "sensor", "unit": "km"}, "Int32": {"datatype": "int32", "description": "An int32", "type": "sensor", "unit": "km"}, "Int64": {"datatype": "int64", "description": "An int64", "type": "sensor", "unit": "km"}, "Int8": {"datatype": "int8", "description": "An int8.", "type": "sensor", "unit": "km"}, "IsBoolean": {"datatype": "boolean", "description": "A boolean", "type": "sensor"}, "String": {"datatype": "string", "description": "A string.", "type": "sensor", "unit": "km"}, "UInt16": {"datatype": "uint16", "description": "A uint16.", "type": "sensor", "unit": "km"}, "UInt32": {"datatype": "uint16", "description": "A uint32.", "type": "sensor", "unit": "km"}, "UInt64": {"datatype": "uint64", "description": "A uint64.", "type": "sensor", "unit": "km"}, "UInt8": {"datatype": "uint8", "description": "A uint8.", "type": "sensor", "unit": "km"}}, "description": "Branch A.", "type": "branch"}}
\ No newline at end of file
+{"A": {"children": {"Double": {"datatype": "double", "description": "A double.", "type": "sensor", "unit": "km"}, "Float": {"datatype": "float", "description": "A float.", "type": "sensor", "unit": "km"}, "Int16": {"datatype": "int16", "description": "An int16.", "type": "sensor", "unit": "km"}, "Int32": {"datatype": "int32", "description": "An int32", "type": "sensor", "unit": "km"}, "Int64": {"datatype": "int64", "description": "An int64", "type": "sensor", "unit": "km"}, "Int8": {"datatype": "int8", "description": "An int8.", "type": "sensor", "unit": "km"}, "IsBoolean": {"datatype": "boolean", "description": "A boolean", "type": "sensor"}, "UInt16": {"datatype": "uint16", "description": "A uint16.", "type": "sensor", "unit": "km"}, "UInt32": {"datatype": "uint16", "description": "A uint32.", "type": "sensor", "unit": "km"}, "UInt64": {"datatype": "uint64", "description": "A uint64.", "type": "sensor", "unit": "km"}, "UInt8": {"datatype": "uint8", "description": "A uint8.", "type": "sensor", "unit": "km"}}, "description": "Branch A.", "type": "branch"}}
\ No newline at end of file
diff --git a/tests/vspec/test_datatypes/expected.jsonschema b/tests/vspec/test_datatypes/expected.jsonschema
index 05b80d74..58f40335 100644
--- a/tests/vspec/test_datatypes/expected.jsonschema
+++ b/tests/vspec/test_datatypes/expected.jsonschema
@@ -1,1 +1,1 @@
-{"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "A", "type": "object", "description": "Branch A.", "properties": {"UInt8": {"description": "A uint8.", "type": "integer"}, "Int8": {"description": "An int8.", "type": "integer"}, "UInt16": {"description": "A uint16.", "type": "integer"}, "Int16": {"description": "An int16.", "type": "integer"}, "UInt32": {"description": "A uint32.", "type": "integer"}, "Int32": {"description": "An int32", "type": "integer"}, "UInt64": {"description": "A uint64.", "type": "integer"}, "Int64": {"description": "An int64", "type": "integer"}, "IsBoolean": {"description": "A boolean", "type": "boolean"}, "Float": {"description": "A float.", "type": "number"}, "Double": {"description": "A double.", "type": "number"}, "String": {"description": "A string.", "type": "string"}}}
\ No newline at end of file
+{"$schema": "https://json-schema.org/draft/2020-12/schema", "title": "A", "type": "object", "description": "Branch A.", "properties": {"UInt8": {"description": "A uint8.", "type": "integer"}, "Int8": {"description": "An int8.", "type": "integer"}, "UInt16": {"description": "A uint16.", "type": "integer"}, "Int16": {"description": "An int16.", "type": "integer"}, "UInt32": {"description": "A uint32.", "type": "integer"}, "Int32": {"description": "An int32", "type": "integer"}, "UInt64": {"description": "A uint64.", "type": "integer"}, "Int64": {"description": "An int64", "type": "integer"}, "IsBoolean": {"description": "A boolean", "type": "boolean"}, "Float": {"description": "A float.", "type": "number"}, "Double": {"description": "A double.", "type": "number"}}}
\ No newline at end of file
diff --git a/tests/vspec/test_datatypes/expected.yaml b/tests/vspec/test_datatypes/expected.yaml
index e3c7acfe..fff9b5ef 100644
--- a/tests/vspec/test_datatypes/expected.yaml
+++ b/tests/vspec/test_datatypes/expected.yaml
@@ -43,12 +43,6 @@ A.IsBoolean:
   description: A boolean
   type: sensor
 
-A.String:
-  datatype: string
-  description: A string.
-  type: sensor
-  unit: km
-
 A.UInt16:
   datatype: uint16
   description: A uint16.
diff --git a/tests/vspec/test_datatypes/test.vspec b/tests/vspec/test_datatypes/test.vspec
index d07562e0..e1dda362 100644
--- a/tests/vspec/test_datatypes/test.vspec
+++ b/tests/vspec/test_datatypes/test.vspec
@@ -67,11 +67,3 @@ A.Double:
   type: sensor
   unit: km
   description: A double.
-  
-A.String:
-  datatype: string
-  type: sensor
-  unit: km
-  description: A string.
-  
-  
diff --git a/tests/vspec/test_datatypes_error/test_datatypes_error.py b/tests/vspec/test_datatypes_error/test_datatypes_error.py
index 79b6e35b..bc1d9b92 100644
--- a/tests/vspec/test_datatypes_error/test_datatypes_error.py
+++ b/tests/vspec/test_datatypes_error/test_datatypes_error.py
@@ -10,33 +10,30 @@
 import subprocess
 import os
 
+
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def test_datatype_error(tmp_path):
     spec = HERE / "test.vspec"
     output = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export json --pretty -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
     process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
 
-    assert (
-        "Following types were referenced in signals but have not been defined: uint7"
-        in process.stdout
-    )
+    print(process.stdout)
+    assert "'uint7' is not a valid datatype" in process.stdout
 
 
 def test_datatype_branch(tmp_path):
     spec = HERE / "test_datatype_branch.vspec"
     output = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export json --pretty -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output} --strict"
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-
-    assert (
-        "cannot have datatype"
-        in process.stdout
-    )
+    print(process.stdout)
+    assert "Unknown extra attribute: 'A':'datatype'" in process.stdout
diff --git a/tests/vspec/test_default_unit/test_default_units.py b/tests/vspec/test_default_unit/test_default_units.py
index 67ae23e1..0b5e7d4e 100644
--- a/tests/vspec/test_default_unit/test_default_units.py
+++ b/tests/vspec/test_default_unit/test_default_units.py
@@ -12,6 +12,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def run_unit(vspec_file, unit_argument, expected_file, tmp_path):
@@ -32,7 +33,7 @@ def run_unit_error(vspec_file, unit_argument, check_message, tmp_path):
 def test_default_ok(tmp_path):
     run_unit(
         HERE / "signals_with_default_units.vspec",
-        "",
+        f"-q {TEST_QUANT}",
         HERE / "expected_default.json",
         tmp_path,
     )
@@ -43,7 +44,7 @@ def test_default_explicit(tmp_path):
     units = HERE / "units.yaml"
     run_unit(
         HERE / "signals_with_default_units.vspec",
-        f"-u {units}",
+        f"-u {units} -q {TEST_QUANT}",
         HERE / "expected_default.json",
         tmp_path,
     )
@@ -53,7 +54,7 @@ def test_default_explicit(tmp_path):
 def test_default_error(tmp_path):
     run_unit_error(
         HERE / "signals_with_default_units.vspec",
-        f"-u {TEST_UNITS}",
-        "Unknown unit",
+        f"-u {TEST_UNITS} -q {TEST_QUANT}",
+        "not a valid unit",
         tmp_path,
     )
diff --git a/tests/vspec/test_default_unit/units.yaml b/tests/vspec/test_default_unit/units.yaml
index 9b287538..f349db3c 100644
--- a/tests/vspec/test_default_unit/units.yaml
+++ b/tests/vspec/test_default_unit/units.yaml
@@ -1,15 +1,17 @@
 # Unit file with default name
 # Same name as https://github.com/COVESA/vehicle_signal_specification/blob/master/spec/units.yaml
-units:
-  x:
-    label: xxxxxxxxx
-    description: Distance in x
-    domain: distance
-  y:
-    label: yyyyyyyyy
-    description: Distance in y
-    domain: distance
-  z:
-    label: zzzzzzzzz
-    description: Distance in z
-    domain: distance
+x:
+  unit: xxxxxxxxx
+  definition: Distance in x
+  quantity: distance
+  allowed-datatypes: ['numeric']
+y:
+  unit: yyyyyyyyy
+  definition: Distance in y
+  quantity: distance
+  allowed-datatypes: ['numeric']
+z:
+  unit: zzzzzzzzz
+  definition: Distance in z
+  quantity: distance
+  allowed-datatypes: ['numeric']
diff --git a/tests/vspec/test_description_error/test_description_error.py b/tests/vspec/test_description_error/test_description_error.py
index 127ace01..ed0c8d39 100644
--- a/tests/vspec/test_description_error/test_description_error.py
+++ b/tests/vspec/test_description_error/test_description_error.py
@@ -12,6 +12,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 @pytest.mark.parametrize(
@@ -28,14 +29,17 @@ def test_description_error(
     vspec_file: str, type_file: str, type_out_file: str, tmp_path
 ):
     output = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty -u {TEST_UNITS}"
+    log = tmp_path / "out.log"
+    cmd = f"vspec --log-file {log} export json --pretty -u {TEST_UNITS} -q {TEST_QUANT}"
     if type_file:
         cmd += f" --types {HERE / type_file}"
     if type_out_file:
         cmd += f" --types-output {tmp_path / type_out_file}"
     cmd += f" --vspec {HERE / vspec_file} --output {output}"
 
-    print(cmd)
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-    assert "Invalid VSS element" in process.stdout
+    log_content = log.read_text()
+    print(log_content)
+    assert "'type': 'missing'" in log_content
+    assert "1 model error(s):" in log_content
diff --git a/tests/vspec/test_extended/test_extended.py b/tests/vspec/test_extended/test_extended.py
index 07e8a600..d1a40866 100644
--- a/tests/vspec/test_extended/test_extended.py
+++ b/tests/vspec/test_extended/test_extended.py
@@ -13,55 +13,58 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 TEST_FILE = HERE / "test.vspec"
-KNOWN_PREFIX = "Known extended attributes: "
-WARNING_PREFIX = "Attribute(s) "
-WARNING_SUFFIX = " in element SignalA not a core"
+KNOWN_PREFIX = "User defined extra attributes: "
+WARNING_PREFIX = "Unknown extra attribute:"
 
 
 @pytest.mark.parametrize(
     "extended_args, known_extended, extended_warning",
     [
         ("", None, "e1, e2, e3"),
-        ("-e e1", "e1", "e2, e3"),
-        ("-e e1 -e e1", "e1, e1", "e2, e3"),  # Duplicated items presented as is
-        ("-e e1 -e e2", "e1, e2", "e3"),
-        ("-e e1 -e e2 -e e3", "e1, e2, e3", None),
-        ("-e e1 -e e2 -e e3 -e e2", "e1, e2, e3, e2", None),
-        ("-e e1 -e e2 -e e3 -e e4", "e1, e2, e3, e4", None),  # also ones that are not used (e4) are presented
-
+        ("-e e1", "('e1',)", "e2, e3"),
+        # Duplicated items presented as is
+        ("-e e1 -e e1", "('e1', 'e1')", "e2, e3"),
+        ("-e e1 -e e2", "('e1', 'e2')", "e3"),
+        ("-e e1 -e e2 -e e3", "('e1', 'e2', 'e3')", None),
+        ("-e e1 -e e2 -e e3 -e e2", "('e1', 'e2', 'e3', 'e2')", None),
+        (
+            "-e e1 -e e2 -e e3 -e e4",
+            "('e1', 'e2', 'e3', 'e4')",
+            None,
+        ),  # also ones that are not used (e4) are presented
         # Some cases like above but using full name
-        ("--extended-attributes e1 --extended-attributes e2", "e1, e2", "e3"),
-        ("--extended-attributes e1 -e e2", "e1, e2", "e3"),
-        ("-e e1 --extended-attributes e2", "e1, e2", "e3"),
+        ("--extended-attributes e1 --extended-attributes e2", "('e1', 'e2')", "e3"),
+        ("--extended-attributes e1 -e e2", "('e1', 'e2')", "e3"),
+        ("-e e1 --extended-attributes e2", "('e1', 'e2')", "e3"),
     ],
 )
 def test_extended_ok(
-    extended_args: str, known_extended: str | None, extended_warning: str | None,
-    tmp_path
+    extended_args: str,
+    known_extended: str | None,
+    extended_warning: str | None,
+    tmp_path,
 ):
-
     output = tmp_path / "out.json"
-    cmd = (
-        f"vspec export json --pretty -u {TEST_UNITS} {extended_args} -s {TEST_FILE} -o {output}"
-    )
+    cmd = f"vspec export json --pretty -u {TEST_UNITS}"
+    cmd += f" -q {TEST_QUANT} {extended_args} -s {TEST_FILE} -o {output}"
 
     # Make sure there is no line break that affects compare
     os.environ["COLUMNS"] = "120"
 
     process = subprocess.run(cmd.split(), capture_output=True, check=True, text=True)
-    print(process.stdout)
 
     if known_extended is not None:
-        check_str = KNOWN_PREFIX+known_extended+" "
+        print(process.stdout)
+        check_str = KNOWN_PREFIX + known_extended + " "
         assert check_str in process.stdout
     else:
         assert KNOWN_PREFIX not in process.stdout
 
     if extended_warning:
-        assert (WARNING_PREFIX + extended_warning + WARNING_SUFFIX) in process.stdout
-    else:
-        assert WARNING_SUFFIX not in process.stdout
+        for i in extended_warning.split(","):
+            assert (WARNING_PREFIX + f" 'A.SignalA':'{i.strip()}'") in process.stdout
 
 
 @pytest.mark.parametrize(
@@ -70,24 +73,20 @@ def test_extended_ok(
         ("-e e1,e2"),
         ("-e e1,e2,e3"),
         ("-e e1,e2 -e e3"),
-
         # Some cases like above but using full name
         ("--extended-attributes e1,e2"),
     ],
 )
-def test_extended_error(
-    extended_args: str,
-    tmp_path
-):
-
+def test_extended_error(extended_args: str, tmp_path):
     output = tmp_path / "out.json"
-    cmd = (
-        f"vspec export json --pretty -u {TEST_UNITS} {extended_args} -s {TEST_FILE} -o {output}"
-    )
+    cmd = f"vspec export json --pretty -u {TEST_UNITS}"
+    cmd += f" -q {TEST_QUANT} {extended_args} -s {TEST_FILE} -o {output}"
 
     # Make sure there is no line break that affects compare
     os.environ["COLUMNS"] = "120"
 
-    process = subprocess.run(cmd.split(), stdout=subprocess.PIPE, text=True, stderr=subprocess.STDOUT)
+    process = subprocess.run(
+        cmd.split(), stdout=subprocess.PIPE, text=True, stderr=subprocess.STDOUT
+    )
     assert process.returncode != 0
     assert "not allowed" in process.stdout
diff --git a/tests/vspec/test_faulty_type/test_faulty_type.py b/tests/vspec/test_faulty_type/test_faulty_type.py
index 0cb97597..dca8e404 100644
--- a/tests/vspec/test_faulty_type/test_faulty_type.py
+++ b/tests/vspec/test_faulty_type/test_faulty_type.py
@@ -11,12 +11,15 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def test_error(tmp_path):
     spec = HERE / "test.vspec"
     output = tmp_path / "out.json"
-    cmd = f"vspec export json -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output}"
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-    assert "Type not allowed: bosch" in process.stdout
+    print(process.stdout)
+    assert "input': 'bosch'" in process.stdout
+    assert "Input should be 'branch'" in process.stdout
diff --git a/tests/vspec/test_generic.py b/tests/vspec/test_generic.py
index 4b6ad935..572d7d3b 100644
--- a/tests/vspec/test_generic.py
+++ b/tests/vspec/test_generic.py
@@ -14,6 +14,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / "test_units.yaml"
+TEST_QUANT = HERE / "test_quantities.yaml"
 
 
 def default_directories() -> list:
@@ -36,8 +37,10 @@ def run_exporter(directory, exporter, tmp_path):
     vspec = directory / "test.vspec"
     output = tmp_path / f"out.{exporter}"
     expected = directory / f"expected.{exporter}"
-    cmd = f"vspec export {exporter} -u {TEST_UNITS} --vspec {vspec} --output {output}"
+    cmd = f"vspec export {exporter} -u {TEST_UNITS} -q {TEST_QUANT} --vspec {vspec} --output {output}"
     subprocess.run(cmd.split(), check=True)
+    print(output)
+    print(expected)
     assert filecmp.cmp(output, expected)
 
 
@@ -45,8 +48,7 @@ def run_exporter(directory, exporter, tmp_path):
 def test_exporters(directory, tmp_path):
     # Run all "supported" exporters, i.e. not those in contrib
     # Exception is "binary", as it is assumed output may vary depending on target
-    exporters = ["json", "jsonschema", "ddsidl",
-                 "csv", "yaml", "franca", "graphql"]
+    exporters = ["json", "jsonschema", "ddsidl", "csv", "yaml", "franca", "graphql"]
 
     for exporter in exporters:
         run_exporter(directory, exporter, tmp_path)
diff --git a/tests/vspec/test_include/test_include.py b/tests/vspec/test_include/test_include.py
index 1568ac77..50fc7714 100644
--- a/tests/vspec/test_include/test_include.py
+++ b/tests/vspec/test_include/test_include.py
@@ -12,13 +12,14 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def test_include(tmp_path):
     spec = HERE / "test.vspec"
     output = tmp_path / "out.json"
     expected = HERE / "expected.json"
-    cmd = f"vspec export json -u {TEST_UNITS} --pretty --vspec {spec} --output {output}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --vspec {spec} --output {output}"
     subprocess.run(cmd.split(), check=True)
     filecmp.cmp(output, expected)
 
@@ -26,10 +27,8 @@ def test_include(tmp_path):
 def test_error(tmp_path):
     spec = HERE / "test_error.vspec"
     output = tmp_path / "out.json"
-    cmd = f"vspec export json -u {TEST_UNITS} --pretty --vspec {spec} --output {output}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --vspec {spec} --output {output}"
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
 
-    print(process.stdout)
-    assert "WARNING  No branch matching" in process.stdout
-    assert "Exception: Invalid VSS model" in process.stderr
+    assert "MultipleRootsException" in process.stderr
diff --git a/tests/vspec/test_instances/expected.csv b/tests/vspec/test_instances/expected.csv
index c3269d81..0207454c 100644
--- a/tests/vspec/test_instances/expected.csv
+++ b/tests/vspec/test_instances/expected.csv
@@ -1,13 +1,13 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.B","branch","","","","","","This description will be duplicated.","This comment will be duplicated","",""
-"A.B.Row1","branch","","","","","","This description will be duplicated.","This comment will be duplicated","",""
-"A.B.Row1.Left","branch","","","","","","This description will be duplicated.","This comment will be duplicated","",""
-"A.B.Row1.Left.C","sensor","int8","","km","","","This description will also exist multiple times.","As well as this comment.","",""
-"A.B.Row1.Right","branch","","","","","","This description will be duplicated.","This comment will be duplicated","",""
-"A.B.Row1.Right.C","sensor","int8","","km","","","This description will also exist multiple times.","As well as this comment.","",""
-"A.B.Row2","branch","","","","","","This description will be duplicated.","This comment will be duplicated","",""
-"A.B.Row2.Left","branch","","","","","","This description will be duplicated.","This comment will be duplicated","",""
-"A.B.Row2.Left.C","sensor","int8","","km","","","This description will also exist multiple times.","As well as this comment.","",""
-"A.B.Row2.Right","branch","","","","","","This description will be duplicated.","This comment will be duplicated","",""
-"A.B.Row2.Right.C","sensor","int8","","km","","","This description will also exist multiple times.","As well as this comment.","",""
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.B,branch,,,,,,This description will be duplicated.,This comment will be duplicated,,
+A.B.Row1,branch,,,,,,This description will be duplicated.,This comment will be duplicated,,
+A.B.Row1.Left,branch,,,,,,This description will be duplicated.,This comment will be duplicated,,
+A.B.Row1.Left.C,sensor,int8,,km,,,This description will also exist multiple times.,As well as this comment.,,
+A.B.Row1.Right,branch,,,,,,This description will be duplicated.,This comment will be duplicated,,
+A.B.Row1.Right.C,sensor,int8,,km,,,This description will also exist multiple times.,As well as this comment.,,
+A.B.Row2,branch,,,,,,This description will be duplicated.,This comment will be duplicated,,
+A.B.Row2.Left,branch,,,,,,This description will be duplicated.,This comment will be duplicated,,
+A.B.Row2.Left.C,sensor,int8,,km,,,This description will also exist multiple times.,As well as this comment.,,
+A.B.Row2.Right,branch,,,,,,This description will be duplicated.,This comment will be duplicated,,
+A.B.Row2.Right.C,sensor,int8,,km,,,This description will also exist multiple times.,As well as this comment.,,
diff --git a/tests/vspec/test_min_max/expected.csv b/tests/vspec/test_min_max/expected.csv
index 6a6f85b0..586c2b41 100644
--- a/tests/vspec/test_min_max/expected.csv
+++ b/tests/vspec/test_min_max/expected.csv
@@ -1,16 +1,16 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.IntNoMinMax","sensor","int8","","","","","No Min Max.","","",""
-"A.IntOnlyMax","sensor","int8","","","","32","Only Max.","","",""
-"A.IntOnlyMin","sensor","int8","","","3","","Only Min.","","",""
-"A.IntMinMax","sensor","int8","","","3","6","Min & Max.","","",""
-"A.IntMaxZero","sensor","int8","","","","0","Max Zero.","","",""
-"A.IntMinZero","sensor","int8","","","0","","Min Zero.","","",""
-"A.FloatNoMinMax","sensor","float","","","","","No Min Max.","","",""
-"A.FloatOnlyMax","sensor","float","","","","32.3","Only Max.","","",""
-"A.FloatOnlyMin","sensor","float","","","-2.5","","Only Min.","","",""
-"A.FloatMinMax","sensor","float","","","-165.56323","236723.4","Min & Max.","","",""
-"A.FloatMaxZero","sensor","float","","","","0.0","Max Zero.","","",""
-"A.FloatMinZero","sensor","float","","","0.0","","Min Zero.","","",""
-"A.FloatMaxZeroInt","sensor","float","","","","0","Max Zero.","","",""
-"A.FloatMinZeroInt","sensor","float","","","0","","Min Zero.","","",""
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.IntNoMinMax,sensor,int8,,,,,No Min Max.,,,
+A.IntOnlyMax,sensor,int8,,,,32,Only Max.,,,
+A.IntOnlyMin,sensor,int8,,,3,,Only Min.,,,
+A.IntMinMax,sensor,int8,,,3,6,Min & Max.,,,
+A.IntMaxZero,sensor,int8,,,,0,Max Zero.,,,
+A.IntMinZero,sensor,int8,,,0,,Min Zero.,,,
+A.FloatNoMinMax,sensor,float,,,,,No Min Max.,,,
+A.FloatOnlyMax,sensor,float,,,,32.3,Only Max.,,,
+A.FloatOnlyMin,sensor,float,,,-2.5,,Only Min.,,,
+A.FloatMinMax,sensor,float,,,-165.56323,236723.4,Min & Max.,,,
+A.FloatMaxZero,sensor,float,,,,0.0,Max Zero.,,,
+A.FloatMinZero,sensor,float,,,0.0,,Min Zero.,,,
+A.FloatMaxZeroInt,sensor,float,,,,0,Max Zero.,,,
+A.FloatMinZeroInt,sensor,float,,,0,,Min Zero.,,,
diff --git a/tests/vspec/test_multiple_type_trees/test_multiple_type_trees.py b/tests/vspec/test_multiple_type_trees/test_multiple_type_trees.py
index 6c0722e6..a395b6b4 100644
--- a/tests/vspec/test_multiple_type_trees/test_multiple_type_trees.py
+++ b/tests/vspec/test_multiple_type_trees/test_multiple_type_trees.py
@@ -11,6 +11,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def test_error(tmp_path):
@@ -22,7 +23,8 @@ def test_error(tmp_path):
     vt1 = HERE / "struct1.vspec"
     vt2 = HERE / "struct2.vspec"
     spec = HERE / "test.vspec"
-    cmd = f"vspec export csv --types {vt1} --types {vt2} -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export csv --types {vt1} --types {vt2}"
+    cmd += f" -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output}"
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-    assert "unknown root node" in process.stderr
+    assert "MultipleTypeTreesException" in process.stderr
diff --git a/tests/vspec/test_no_expand/expected_overlay_no_expand.json b/tests/vspec/test_no_expand/expected_overlay_no_expand.json
index a3c5827d..bed28238 100644
--- a/tests/vspec/test_no_expand/expected_overlay_no_expand.json
+++ b/tests/vspec/test_no_expand/expected_overlay_no_expand.json
@@ -60,7 +60,9 @@
           }
         },
         "description": "Branch with instance range.",
-        "instances": "Test[1,4]",
+        "instances": [
+          "Test[1,4]"
+        ],
         "type": "branch"
       },
       "D": {
diff --git a/tests/vspec/test_no_expand/test_no_expand.py b/tests/vspec/test_no_expand/test_no_expand.py
index 6187980d..1edc609f 100644
--- a/tests/vspec/test_no_expand/test_no_expand.py
+++ b/tests/vspec/test_no_expand/test_no_expand.py
@@ -14,6 +14,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 # Overlay tests, just showing for JSON
@@ -34,8 +35,9 @@ def test_json_overlay(no_expand, comparison_file, tmp_path):
     cmd = "vspec export json"
     if no_expand:
         cmd += " --no-expand"
-    cmd += f" --pretty -u {TEST_UNITS} --vspec {spec} -l {overlay} --output {output}"
+    cmd += f" --pretty -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} -l {overlay} --output {output}"
 
-    subprocess.run(cmd.split(), check=True, capture_output=True, text=True)
+    process = subprocess.run(cmd.split(), capture_output=True, text=True)
+    assert process.returncode == 0
     expected = HERE / comparison_file
     assert filecmp.cmp(output, expected)
diff --git a/tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec b/tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec
index 5a34b90e..7390256e 100644
--- a/tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec
+++ b/tests/vspec/test_node_removal/test_files/test_del_instance_overlay.vspec
@@ -9,3 +9,4 @@
 A.C.Instance2:
   type: branch
   delete: true
+  description: Delete
diff --git a/tests/vspec/test_node_removal/test_files/test_del_wrong_instance_overlay.vspec b/tests/vspec/test_node_removal/test_files/test_del_wrong_instance_overlay.vspec
deleted file mode 100644
index 7c3448b1..00000000
--- a/tests/vspec/test_node_removal/test_files/test_del_wrong_instance_overlay.vspec
+++ /dev/null
@@ -1,11 +0,0 @@
-# Copyright (c) 2024 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0, which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-A.C.Instance3:
-  type: branch
-  delete: true
diff --git a/tests/vspec/test_node_removal/test_node_removal.py b/tests/vspec/test_node_removal/test_node_removal.py
index 04f781e0..9905f8ce 100644
--- a/tests/vspec/test_node_removal/test_node_removal.py
+++ b/tests/vspec/test_node_removal/test_node_removal.py
@@ -19,10 +19,11 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def get_cla(test_file: str, out_file: str, overlay: Optional[str]):
-    args = f"-u {TEST_UNITS}"
+    args = f"-u {TEST_UNITS} -q {TEST_QUANT}"
     if overlay:
         args += f" -l {overlay}"
     args += f" --vspec {test_file} --output {out_file}"
@@ -197,7 +198,6 @@ def test_deleted_branch(exporter: str, out_file: str, overlay: Optional[str], tm
     "overlay",
     [
         "test_files/test_del_instance_overlay.vspec",
-        "test_files/test_del_wrong_instance_overlay.vspec",
     ],
 )
 def test_deleted_instance(
@@ -214,11 +214,15 @@ def test_deleted_instance(
     cmd = exporter.split() + get_cla(spec, str(output), str(ov)).split()
     if "binary" in exporter:
         cmd.extend(["-b", HERE / "../../../binary/binarytool.so"])
+
     process = subprocess.run(cmd, capture_output=True, text=True)
+    print(process.stdout)
+    print(process.stderr)
 
     if "wrong" in overlay:
         assert process.returncode != 0
     else:
+        print(process.stdout)
         result_file = output.read_text()
 
         remaining_nodes = [
@@ -249,8 +253,7 @@ def test_deleted_instance(
                 assert node in result_file
         elif exporter == "vspec export graphql":
             assert "A.C.Instance2".replace(".", "_") not in result_file
-            remaining_nodes = [node.replace(".", "_")
-                               for node in remaining_nodes]
+            remaining_nodes = [node.replace(".", "_") for node in remaining_nodes]
             for node in remaining_nodes:
                 assert node in result_file
         else:
diff --git a/tests/vspec/test_overlay/overlay_implicit_branch_no_description.vspec b/tests/vspec/test_overlay/overlay_implicit_branch_no_description.vspec
index 07cc6bda..982a0db9 100644
--- a/tests/vspec/test_overlay/overlay_implicit_branch_no_description.vspec
+++ b/tests/vspec/test_overlay/overlay_implicit_branch_no_description.vspec
@@ -31,4 +31,3 @@ A.AA.SignalAA:
 A.AB.SignalAB:
   type: sensor
   datatype: uint8
-  description: New signal in new branch.
diff --git a/tests/vspec/test_overlay/test_overlay.py b/tests/vspec/test_overlay/test_overlay.py
index c6025224..f1d8d2ad 100644
--- a/tests/vspec/test_overlay/test_overlay.py
+++ b/tests/vspec/test_overlay/test_overlay.py
@@ -12,6 +12,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 # Only running json exporter, overlay-functionality should be independent
@@ -21,9 +22,8 @@ def run_overlay(overlay_prefix, tmp_path):
     overlay_name = f"overlay_{overlay_prefix}.vspec"
     overlay = HERE / overlay_name
     output = tmp_path / "out.json"
-    cmd = (
-        f"vspec export json --pretty -u {TEST_UNITS} -e dbc --vspec {spec} -l {overlay} --output {output}"
-    )
+    cmd = f"vspec export json --pretty -u {TEST_UNITS} -q {TEST_QUANT}"
+    cmd += f" -e dbc --vspec {spec} -l {overlay} --output {output}"
     subprocess.run(cmd.split(), check=True)
     expected = HERE / f"expected_{overlay_prefix}.json"
     assert filecmp.cmp(output, expected)
@@ -49,20 +49,28 @@ def test_overlay_error(tmp_path):
     overlay = HERE / "overlay_error.vspec"
     output = tmp_path / "out.json"
     spec = HERE / "test.vspec"
-    cmd = f"vspec export json --pretty -u {TEST_UNITS} -l {overlay} --vspec {spec} --output {output}"
+    cmd = f"vspec export json --pretty -u {TEST_UNITS}"
+    cmd += f" -q {TEST_QUANT} -l {overlay} --vspec {spec} --output {output}"
     env = os.environ.copy()
     env["COLUMNS"] = "300"
     process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
-    assert "need to have a datatype declared" in process.stdout
+    print(process.stdout)
+    assert "'A.SignalXXXX' has 1 model error(s)" in process.stdout
+    assert "'type': 'missing'" in process.stdout
+    assert "datatype" in process.stdout
 
 
 def test_overlay_branch_error(tmp_path):
     overlay = HERE / "overlay_implicit_branch_no_description.vspec"
     output = tmp_path / "out.json"
     spec = HERE / "test.vspec"
-    cmd = f"vspec export json --pretty -u {TEST_UNITS} -l {overlay} --vspec {spec} --output {output}"
+    cmd = f"vspec export json --pretty -u {TEST_UNITS}"
+    cmd += f" -q {TEST_QUANT} -l {overlay} --vspec {spec} --output {output}"
 
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-    assert "Invalid VSS element AB, must have description" in process.stdout
+    print(process.stdout)
+    assert "'A.AB' has 1 model error(s)" in process.stdout
+    assert "'type': 'missing'" in process.stdout
+    assert "description" in process.stdout
diff --git a/tests/vspec/test_overlay_on_instance/expected.json b/tests/vspec/test_overlay_on_instance/expected.json
index 955d68fe..977128e8 100644
--- a/tests/vspec/test_overlay_on_instance/expected.json
+++ b/tests/vspec/test_overlay_on_instance/expected.json
@@ -155,25 +155,6 @@
             "comment": "An example comment",
             "description": "Branch A.B, instances defined in list",
             "type": "branch"
-          },
-          "Row5": {
-            "children": {
-              "Right": {
-                "children": {
-                  "C": {
-                    "datatype": "float",
-                    "description": "New Muhaha",
-                    "my_id": "Landskrona",
-                    "type": "sensor",
-                    "unit": "km"
-                  }
-                },
-                "description": "Also mandatory!",
-                "type": "branch"
-              }
-            },
-            "description": "Is mandatory!",
-            "type": "branch"
           }
         },
         "comment": "An example comment",
diff --git a/tests/vspec/test_overlay_on_instance/overlay_2.vspec b/tests/vspec/test_overlay_on_instance/overlay_2.vspec
index aaa65d2d..3e392f7d 100644
--- a/tests/vspec/test_overlay_on_instance/overlay_2.vspec
+++ b/tests/vspec/test_overlay_on_instance/overlay_2.vspec
@@ -50,22 +50,22 @@ A.B.Row3.Right.C:
   datatype: int8
   my_id: Lund
 
-# Adding one outside instance range, shall not inherit original comment
-# Row5 and Row5.Right does not exist in standard tree, so explicit definition with description needed
-A.B.Row5:
-  type: branch
-  description: Is mandatory!
-
-A.B.Row5.Right:
-  type: branch
-  description: Also mandatory!
-
-A.B.Row5.Right.C:
-  datatype: float
-  type: sensor
-  unit: km
-  my_id: Landskrona
-  description: New Muhaha
+# Adding one outside instance range is not handled in a special
+# way and therefore not allowed
+# A.B.Row5:
+#   type: branch
+#   description: Is mandatory!
+#
+# A.B.Row5.Right:
+#   type: branch
+#   description: Also mandatory!
+#
+# A.B.Row5.Right.C:
+#   datatype: float
+#   type: sensor
+#   unit: km
+#   my_id: Landskrona
+#   description: New Muhaha
 
 #Shall inherit comment
 A.S.Front.T:
@@ -116,8 +116,6 @@ A.S.Front.TT.Left.TTT.TTTY:
   my_id: "Extended attribute"
 
 
-
-
 # Just adding a comment deep down - check that two level branch  (TTT.T4) after instantiation works
 
 A.S.Front.TT.Right.TTT.T4.TTTTR:
diff --git a/tests/vspec/test_overlay_on_instance/test_overlay_on_instance.py b/tests/vspec/test_overlay_on_instance/test_overlay_on_instance.py
index 729ff614..92ec6f42 100644
--- a/tests/vspec/test_overlay_on_instance/test_overlay_on_instance.py
+++ b/tests/vspec/test_overlay_on_instance/test_overlay_on_instance.py
@@ -12,6 +12,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 # Only running json exporter, overlay-functionality should be independent of selected exporter
@@ -20,25 +21,8 @@ def test_expanded_overlay(tmp_path):
     overlay1 = HERE / "overlay_1.vspec"
     overlay2 = HERE / "overlay_2.vspec"
     output = tmp_path / "out.json"
-    cmd = f"vspec export json -e my_id --pretty -u {TEST_UNITS}"
+    cmd = f"vspec export json -e my_id --pretty -u {TEST_UNITS} -q {TEST_QUANT}"
     cmd += f" --vspec {spec} -l {overlay1} -l {overlay2} --output {output}"
     subprocess.run(cmd.split(), check=True)
-
     expected = HERE / "expected.json"
     assert filecmp.cmp(output, expected)
-
-
-def test_expanded_overlay_no_type_datatype(tmp_path):
-    """
-    This test shows current limitation on type/datatype lookup.
-    We cannot do lookup on expanded names containing instances.
-    That would require quite some more advanced lookup considering instance declarations.
-    Not impossible, but more complex.
-    """
-    spec = HERE / "test.vspec"
-    overlay = HERE / "overlay_no_type_datatype.vspec"
-    output = tmp_path / "out.json"
-    cmd = f"vspec export json -e my_id --pretty -u {TEST_UNITS} --vspec {spec} -l {overlay} --output {output}"
-    process = subprocess.run(cmd.split(), capture_output=True, text=True)
-    assert process.returncode != 0
-    assert "No type specified for A.B.Row1.Left.C" in process.stdout
diff --git a/tests/vspec/test_overlay_struct/expected.csv b/tests/vspec/test_overlay_struct/expected.csv
index 24e1db71..9086d6ac 100644
--- a/tests/vspec/test_overlay_struct/expected.csv
+++ b/tests/vspec/test_overlay_struct/expected.csv
@@ -1,11 +1,11 @@
-"Node","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.SignalA","sensor","Types.Struct1","","","","","This is the first signal.","","",""
-"A.SignalB","sensor","Types.Struct2","","","","","This is the second signal.","","",""
-"Types","branch","","","","","","Top-level branch for vehicle data types.","","",""
-"Types.Struct1","struct","","","","","","A struct with datatype property defined.","","",""
-"Types.Struct1.x","property","double","","","","","x property","","",""
-"Types.Struct1.y","property","uint16","","","","","y property","","",""
-"Types.Struct2","struct","","","","","","A struct with datatype property defined.","","",""
-"Types.Struct2.c","property","double","","","","","x property","","",""
-"Types.Struct2.d","property","uint16","","","","","y property","","",""
+Node,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.SignalA,sensor,Types.Struct1,,,,,This is the first signal.,,,
+A.SignalB,sensor,Types.Struct2,,,,,This is the second signal.,,,
+Types,branch,,,,,,Top-level branch for vehicle data types.,,,
+Types.Struct1,struct,,,,,,A struct with datatype property defined.,,,
+Types.Struct1.x,property,double,,,,,x property,,,
+Types.Struct1.y,property,uint16,,,,,y property,,,
+Types.Struct2,struct,,,,,,A struct with datatype property defined.,,,
+Types.Struct2.c,property,double,,,,,x property,,,
+Types.Struct2.d,property,uint16,,,,,y property,,,
diff --git a/tests/vspec/test_overlay_struct/test_overlay_struct.py b/tests/vspec/test_overlay_struct/test_overlay_struct.py
index 30dc6e41..9254e1e7 100644
--- a/tests/vspec/test_overlay_struct/test_overlay_struct.py
+++ b/tests/vspec/test_overlay_struct/test_overlay_struct.py
@@ -14,6 +14,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 # First test case on all supported exportes
@@ -39,10 +40,14 @@ def test_overlay_struct(format, signals_out, expected_signal, tmp_path):
     cmd = f"vspec export {format}"
     if format == "json":
         cmd += " --pretty"
-    cmd += f" --types {struct1} --types {struct2} -u {TEST_UNITS} -l {overlay} --vspec {spec} --output {output}"
+    cmd += f" --types {struct1} --types {struct2} -u {TEST_UNITS} -q {TEST_QUANT}"
+    cmd += f" -l {overlay} --vspec {spec} --output {output}"
 
-    subprocess.run(cmd.split(), cwd=tmp_path, check=True)
+    process = subprocess.run(cmd.split(), cwd=tmp_path)
+    print(cmd)
+    assert process.returncode == 0
     expected = HERE / expected_signal
+
     assert filecmp.cmp(output, expected)
 
     if format == "protobuf":
@@ -66,7 +71,10 @@ def test_overlay_struct_using_struct(format, signals_out, expected_signal, tmp_p
     spec = HERE / "test.vspec"
     output = tmp_path / signals_out
     cmd = f"vspec export {format} --pretty --types {struct1} --types {struct2}"
-    cmd += f" -u {TEST_UNITS} --vspec {spec} -l {overlay} --output {output}"
-    subprocess.run(cmd.split(), check=True)
+    cmd += f" -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} -l {overlay} --output {output}"
+    process = subprocess.run(cmd.split())
+    print(process.stdout)
+    print(cmd)
+    assert process.returncode == 0
     expected = HERE / expected_signal
     assert filecmp.cmp(output, expected)
diff --git a/tests/vspec/test_overlay_struct_array/expected.csv b/tests/vspec/test_overlay_struct_array/expected.csv
index a91abdba..74ea60c8 100644
--- a/tests/vspec/test_overlay_struct_array/expected.csv
+++ b/tests/vspec/test_overlay_struct_array/expected.csv
@@ -1,8 +1,8 @@
-"Node","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.SignalA","sensor","Types.Struct1[]","","","","","This is the first signal.","","",""
-"A.SignalB","sensor","Types.Struct1[]","","","","","This is the second signal.","","",""
-"Types","branch","","","","","","Top-level branch for vehicle data types.","","",""
-"Types.Struct1","struct","","","","","","A struct with datatype property defined.","","",""
-"Types.Struct1.x","property","double","","","","","x property","","",""
-"Types.Struct1.y","property","uint16","","","","","y property","","",""
+Node,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.SignalA,sensor,Types.Struct1[],,,,,This is the first signal.,,,
+A.SignalB,sensor,Types.Struct1[],,,,,This is the second signal.,,,
+Types,branch,,,,,,Top-level branch for vehicle data types.,,,
+Types.Struct1,struct,,,,,,A struct with datatype property defined.,,,
+Types.Struct1.x,property,double,,,,,x property,,,
+Types.Struct1.y,property,uint16,,,,,y property,,,
diff --git a/tests/vspec/test_overlay_struct_array/test_overlay_struct_array.py b/tests/vspec/test_overlay_struct_array/test_overlay_struct_array.py
index 97a87a2b..77128026 100644
--- a/tests/vspec/test_overlay_struct_array/test_overlay_struct_array.py
+++ b/tests/vspec/test_overlay_struct_array/test_overlay_struct_array.py
@@ -15,6 +15,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 # First test case on all supported exportes
@@ -38,10 +39,11 @@ def test_overlay_struct_array(format, signals_out, expected_signal, tmp_path):
     cmd = f"vspec export {format}"
     if format == "json":
         cmd += " --pretty"
-    cmd += f" --types {struct} -u {TEST_UNITS} --vspec {spec} -l {overlay} --output {output}"
+    cmd += f" --types {struct} -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} -l {overlay} --output {output}"
     subprocess.run(cmd.split(), cwd=tmp_path, check=True)
 
     expected = HERE / expected_signal
+    print(cmd)
     assert filecmp.cmp(output, expected)
 
     if format == "protobuf":
diff --git a/tests/vspec/test_quantities.yaml b/tests/vspec/test_quantities.yaml
index b859ac44..f2d1f523 100644
--- a/tests/vspec/test_quantities.yaml
+++ b/tests/vspec/test_quantities.yaml
@@ -7,3 +7,14 @@ length:
 temperature:
   definition: Partial derivative of internal energy with respect to entropy at constant volume
               and constant number of particles in the system (ISO 80000-3:2019)
+distance:
+  definition: Well, it's a distance
+
+angular-speed:
+  definition: Test
+
+rotational-speed:
+  definition: Test
+
+relation:
+  definition: Test
diff --git a/tests/vspec/test_static_uids/test_static_uids.py b/tests/vspec/test_static_uids/test_static_uids.py
index e56dcead..6cf1e08b 100644
--- a/tests/vspec/test_static_uids/test_static_uids.py
+++ b/tests/vspec/test_static_uids/test_static_uids.py
@@ -15,71 +15,67 @@
 import os
 import subprocess
 import pytest
-import vss_tools.vspec as vspec
 
 import vss_tools.vspec.vssexporters.vss2id as vss2id
 import yaml
 
-from vss_tools.vspec.model.constants import VSSDataType, VSSTreeType, VSSUnit
-from vss_tools.vspec.model.vsstree import VSSNode
 from vss_tools.vspec.utils.idgen_utils import get_all_keys_values
+from vss_tools.vspec.tree import VSSNode
+from vss_tools.vspec.datatypes import Datatypes
+from vss_tools.vspec.main import get_trees
 
 from pathlib import Path
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
-def get_cla_test(test_file: str, tmp_path: Path, overlay: str | None = None):
+def get_cla_test(test_file: Path, tmp_path: Path, overlay: str | None = None):
     args = f"--vspec {test_file}"
     output = tmp_path / "out.vspec"
     if overlay:
         args += f" -l {overlay}"
     validation = HERE / "validation_vspecs/validation.vspec"
     units = HERE / "test_vspecs/units.yaml"
-    args += f" --output {output} --validate-static-uid {validation} -u {units}"
+    args += f" --output {output} --validate-static-uid {validation} -u {units} -q {TEST_QUANT}"
     return args
 
 
 def get_test_node(
-    node_name: str,
-    unit: str,
-    datatype: VSSDataType,
-    allowed: str,
-    minimum: str,
-    maximum: str,
+    node_name,
+    unit,
+    datatype,
+    allowed,
+    minimum,
+    maximum,
 ) -> VSSNode:
     source = {
         "description": "some desc",
         "type": "branch",
-        "$file_name$": "testfile",
+        "unit": unit,
+        "datatype": datatype,
+        "min": minimum,
+        "max": maximum,
+        "allowed": allowed,
     }
-    node = VSSNode(node_name, source,
-                   VSSTreeType.SIGNAL_TREE.available_types())
-    node.unit = VSSUnit(unit)
-    node.data_type_str = datatype.value
-    node.validate_and_set_datatype()
-    node.allowed = allowed
-    node.min = minimum
-    node.max = maximum
-
-    return node
+    return VSSNode(node_name, None, source)
 
 
 @pytest.mark.parametrize(
     "node_name, unit, datatype, allowed, minimum, maximum, result_static_uid",
     [
-        ("TestNode", "m", VSSDataType.UINT16, "", 0, 10000, "A1D565B2"),
-        ("TestNode", "mm", VSSDataType.UINT32, "", "", "", "B5D7A8FA"),
-        ("TestUnit", "degrees/s", VSSDataType.FLOAT, "", "", "", "DEA9138C"),
-        ("TestMinMax", "percent", VSSDataType.UINT8, "", 0, 100, "88FC5491"),
-        ("TestEnum", "m", VSSDataType.STRING, ["YES, NO"], "", "", "06AEB370"),
+        ("TestNode", "m", Datatypes.UINT16[0], [], 0, 10000, "A1D565B2"),
+        ("TestNode", "mm", Datatypes.UINT32[0], [], None, None, "B5D7A8FA"),
+        ("TestUnit", "degrees/s", Datatypes.FLOAT[0], [], None, None, "DEA9138C"),
+        ("TestMinMax", "percent", Datatypes.UINT8[0], [], 0, 100, "88FC5491"),
+        ("TestEnum", "m", Datatypes.STRING[0], ["YES, NO"], None, None, "06AEB370"),
     ],
 )
 def test_generate_id(
     node_name: str,
     unit: str,
-    datatype: VSSDataType,
+    datatype: Datatypes,
     allowed: str,
     minimum: str,
     maximum: str,
@@ -104,10 +100,10 @@ def test_strict_mode(
     node_case_sensitive = get_test_node(
         node_name=node_name_case_sensitive,
         unit="m",
-        datatype=VSSDataType.FLOAT,
-        allowed="",
-        minimum="",
-        maximum="",
+        datatype=Datatypes.FLOAT[0],
+        allowed=[],
+        minimum=None,
+        maximum=None,
     )
     result_case_sensitive, _ = vss2id.generate_split_id(
         node_case_sensitive, id_counter=0, strict_mode=strict_mode
@@ -116,10 +112,10 @@ def test_strict_mode(
     node_case_insensitive = get_test_node(
         node_name=node_name_case_insensitive,
         unit="m",
-        datatype=VSSDataType.FLOAT,
-        allowed="",
-        minimum="",
-        maximum="",
+        datatype=Datatypes.FLOAT[0],
+        allowed=[],
+        minimum=None,
+        maximum=None,
     )
     result_case_insensitive, _ = vss2id.generate_split_id(
         node_case_insensitive, id_counter=0, strict_mode=strict_mode
@@ -138,16 +134,13 @@ def test_strict_mode(
 def test_export_node(
     request: pytest.FixtureRequest,
     test_file: str,
-    validation_file: str,
+    validation_file,
 ):
     vspec_file = HERE / test_file
     validation_file = HERE / validation_file
-    units = str(HERE / "test_vspecs/units.yaml")
+    unit_files = (HERE / "test_vspecs/units.yaml",)
 
-    vspec.load_units(str(vspec_file), [units])
-    tree = vspec.load_tree(
-        str(vspec_file), include_paths=["."], tree_type=VSSTreeType.SIGNAL_TREE
-    )
+    tree, _ = get_trees(vspec=vspec_file, units=unit_files, quantities=(TEST_QUANT,))
     yaml_dict: Dict[str, str] = {}
     vss2id.export_node(yaml_dict, tree, id_counter=0, strict_mode=False)
 
@@ -167,19 +160,16 @@ def test_export_node(
     ],
 )
 def test_duplicate_hash(caplog: pytest.LogCaptureFixture, children_names: list):
-    tree = get_test_node("TestNode", "m", VSSDataType.UINT32, "", "", "")
-    child_node = get_test_node(
-        children_names[0], "m", VSSDataType.UINT32, "", "", "")
-    child_node2 = get_test_node(
-        children_names[1], "m", VSSDataType.UINT32, "", "", "")
+    tree = get_test_node("TestNode", "m", Datatypes.UINT32[0], "", "", "")
+    child_node = get_test_node(children_names[0], "m", Datatypes.UINT32[0], "", "", "")
+    child_node2 = get_test_node(children_names[1], "m", Datatypes.UINT32[0], "", "", "")
     tree.children = [child_node, child_node2]
 
     yaml_dict: Dict[str, dict] = {}
     if children_names[0] == children_names[1]:
         # assert system exit and log
         with pytest.raises(SystemExit) as pytest_wrapped_e:
-            vss2id.export_node(
-                yaml_dict, tree, id_counter=0, strict_mode=False)
+            vss2id.export_node(yaml_dict, tree, id_counter=0, strict_mode=False)
         assert pytest_wrapped_e.type == SystemExit
         assert pytest_wrapped_e.value.code == -1
         assert len(caplog.records) == 1 and all(
@@ -199,7 +189,7 @@ def test_duplicate_hash(caplog: pytest.LogCaptureFixture, children_names: list):
 
 
 def test_full_script(caplog: pytest.LogCaptureFixture, tmp_path):
-    test_file: str = str(HERE / "test_vspecs/test.vspec")
+    test_file = HERE / "test_vspecs/test.vspec"
     cmd = "vspec export id".split()
     clas = shlex.split(get_cla_test(test_file, tmp_path))
     cmd += clas
@@ -218,7 +208,8 @@ def test_semantic(caplog: pytest.LogCaptureFixture, validation_file: str, tmp_pa
     spec = HERE / "test_vspecs/test.vspec"
     output = tmp_path / "out.vspec"
     validation = HERE / validation_file
-    args = f"vspec export id --vspec {spec} --output {output} --validate-static-uid {validation}"
+    args = f"vspec export id --vspec {spec} --output {output}"
+    args += f" --validate-static-uid {validation} -q {TEST_QUANT}"
     process = subprocess.run(args.split(), capture_output=True, text=True)
     assert "SEMANTIC NAME CHANGE" in process.stdout
 
@@ -309,13 +300,13 @@ def test_deleted_attribute(caplog: pytest.LogCaptureFixture, tmp_path):
 
 
 def test_overlay(caplog: pytest.LogCaptureFixture, tmp_path):
-
     spec = HERE / "test_vspecs/test.vspec"
     overlay = HERE / "test_vspecs/test_overlay.vspec"
     cmd = "vspec export id".split()
     clas = shlex.split(get_cla_test(spec, tmp_path, overlay))
     cmd += clas
     process = subprocess.run(cmd, capture_output=True, text=True)
+    print(process.stdout)
     assert "ADDED ATTRIBUTE" in process.stdout
 
     output = tmp_path / "out.vspec"
diff --git a/tests/vspec/test_static_uids/test_vspecs/units.yaml b/tests/vspec/test_static_uids/test_vspecs/units.yaml
index e725c8f0..2d055379 100644
--- a/tests/vspec/test_static_uids/test_vspecs/units.yaml
+++ b/tests/vspec/test_static_uids/test_vspecs/units.yaml
@@ -1,22 +1,25 @@
-# Taken from the units.yaml of VSS catalog
-units:
-  mm:
-    label: millimeter
-    description: Distance measured in millimeters
-    domain: distance
-  m:
-    label: meter
-    description: Distance measured in meters
-    domain: distance
-  degrees/s:
-    label: degree per second
-    description: Angular speed measured in degrees per second
-    domain: angular speed
-  rpm:
-    label: revolutions per minute
-    description: Rotational speed measured in revolutions per minute
-    domain: rotational speed
-  percent:
-    label: percent
-    description: Relation measured in percent
-    domain: relation
+mm:
+  unit: millimeter
+  definition: Distance measured in millimeters
+  quantity: distance
+  allowed-datatypes: [numeric]
+m:
+  unit: meter
+  definition: Distance measured in meters
+  quantity: distance
+  allowed-datatypes: [numeric]
+degrees/s:
+  unit: degree per second
+  definition: Angular speed measured in degrees per second
+  quantity: angular-speed
+  allowed-datatypes: [numeric]
+rpm:
+  unit: revolutions per minute
+  definition: Rotational speed measured in revolutions per minute
+  quantity: rotational-speed
+  allowed-datatypes: [numeric]
+percent:
+  unit: percent
+  definition: Relation measured in percent
+  quantity: relation
+  allowed-datatypes: [numeric]
diff --git a/tests/vspec/test_static_uids/validation.yaml b/tests/vspec/test_static_uids/validation.yaml
index d7a6dda6..59d6502c 100644
--- a/tests/vspec/test_static_uids/validation.yaml
+++ b/tests/vspec/test_static_uids/validation.yaml
@@ -23,8 +23,8 @@ A.B.IsLeaf:
 A.B.Max:
   datatype: uint8
   description: A leaf that uses a maximum value.
-  min: 0
   max: 100
+  min: 0
   staticUID: '0xCF708DCC'
   type: sensor
   unit: percent
@@ -58,7 +58,8 @@ A.Int16:
   unit: rpm
 A.String:
   datatype: string
-  deprecation: This is test deprecation, let's say it used to be called Str instead String.
+  deprecation: This is test deprecation, let's say it used to be called Str instead
+    String.
   description: A.String is a leaf of A of datatype string
   staticUID: '0x49A622FC'
   type: sensor
diff --git a/tests/vspec/test_strict/correct_boolean.vspec b/tests/vspec/test_strict/correct_boolean.vspec
index 1e915836..da1df65a 100644
--- a/tests/vspec/test_strict/correct_boolean.vspec
+++ b/tests/vspec/test_strict/correct_boolean.vspec
@@ -13,5 +13,4 @@ A:
 A.IsSomething:
   datatype: boolean
   type: sensor
-  unit: km
   description: OK name for a boolean!
diff --git a/tests/vspec/test_strict/faulty_name_boolean.vspec b/tests/vspec/test_strict/faulty_name_boolean.vspec
index 04abd90f..3f901486 100644
--- a/tests/vspec/test_strict/faulty_name_boolean.vspec
+++ b/tests/vspec/test_strict/faulty_name_boolean.vspec
@@ -13,5 +13,4 @@ A:
 A.NotStartingWithIs:
   datatype: boolean
   type: sensor
-  unit: km
   description: Faulty name for boolean (if for VSS catalog)!
diff --git a/tests/vspec/test_strict/test_strict.py b/tests/vspec/test_strict/test_strict.py
index 3e7b7a9d..eebb0c12 100644
--- a/tests/vspec/test_strict/test_strict.py
+++ b/tests/vspec/test_strict/test_strict.py
@@ -13,6 +13,7 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 @pytest.mark.parametrize(
@@ -27,24 +28,25 @@
 def test_not_strict(vspec_file: str, tmp_path):
     spec = HERE / vspec_file
     output = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export json --pretty -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env, check=True)
-    assert "You asked for strict checking. Terminating" not in process.stdout
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
+    print(vspec_file)
+    print(process.stdout)
+    assert process.returncode == 0
 
 
 @pytest.mark.parametrize("vspec_file", ["correct.vspec", "correct_boolean.vspec"])
 def test_strict_ok(vspec_file: str, tmp_path):
     spec = HERE / vspec_file
     output = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty --strict -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export json --pretty --strict -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env, check=True)
-    assert "You asked for strict checking. Terminating" not in process.stdout
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
+    print(process.stdout)
+    assert process.returncode == 0
 
 
 @pytest.mark.parametrize(
@@ -53,10 +55,8 @@ def test_strict_ok(vspec_file: str, tmp_path):
 def test_strict_error(vspec_file: str, tmp_path):
     spec = HERE / vspec_file
     output = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty --strict -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export json --pretty --strict -u {TEST_UNITS} -q {TEST_QUANT} --vspec {spec} --output {output}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env)
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
-    assert "You asked for strict checking. Terminating" in process.stdout
diff --git a/tests/vspec/test_struct_as_root/test_struct_as_root.py b/tests/vspec/test_struct_as_root/test_struct_as_root.py
index 17ab309d..ba5fdd09 100644
--- a/tests/vspec/test_struct_as_root/test_struct_as_root.py
+++ b/tests/vspec/test_struct_as_root/test_struct_as_root.py
@@ -11,13 +11,15 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def test_struct_as_root(tmp_path):
     struct = HERE / "struct1.vspec"
     spec = HERE / "test.vspec"
     output = tmp_path / "out.csv"
-    cmd = f"vspec export csv --types {struct} -u {TEST_UNITS} --vspec {spec} --output {output}"
+    cmd = f"vspec export csv --types {struct} -u {TEST_UNITS}"
+    cmd += f" -q {TEST_QUANT} --vspec {spec} --output {output}"
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-    assert "Root node Struct1 is not of branch type" in process.stdout
+    assert "RootTypesException" in process.stderr
diff --git a/tests/vspec/test_structs/expected-signals-types.csv b/tests/vspec/test_structs/expected-signals-types.csv
index 53c6ebfe..59831eb6 100644
--- a/tests/vspec/test_structs/expected-signals-types.csv
+++ b/tests/vspec/test_structs/expected-signals-types.csv
@@ -1,17 +1,17 @@
-"Node","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.UInt8","sensor","uint8","","km","","","A uint8.","","",""
-"A.ParentStructSensor","sensor","VehicleDataTypes.TestBranch1.ParentStruct","","","","","A rich sensor with user-defined data type.","","",""
-"A.NestedStructSensor","sensor","VehicleDataTypes.TestBranch1.NestedStruct","","","","","A rich sensor with user-defined data type.","","",""
-"VehicleDataTypes","branch","","","","","","Top-level branch for vehicle data types.","","",""
-"VehicleDataTypes.TestBranch1","branch","","","","","","Test branch with structs and properties definitions","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct","struct","","","","","","A struct that is going to be used within another struct - Nested","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct.x","property","double","","","-10","","x property","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct.y","property","double","","","","10","y property","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct.z","property","double","","","","","z property","","","1"
-"VehicleDataTypes.TestBranch1.ParentStruct","struct","","","","","","A struct that is going to contain properties that are structs themselves","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.x_property","property","VehicleDataTypes.TestBranch1.NestedStruct","","","","","A property of struct-type. The struct name is specified relative to the branch","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.y_property","property","VehicleDataTypes.TestBranch1.NestedStruct","","","","","A property of struct-type. The struct name is specified as a fully qualified name","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.x_properties","property","VehicleDataTypes.TestBranch1.NestedStruct[]","","","","","A property of struct-type array and an arraysize","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.y_properties","property","VehicleDataTypes.TestBranch1.NestedStruct[]","","","","","A property of struct-type array and no arraysize","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.z_property","property","double","","","","","A primitive property","","",""
+Node,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.UInt8,sensor,uint8,,km,,,A uint8.,,,
+A.ParentStructSensor,sensor,VehicleDataTypes.TestBranch1.ParentStruct,,,,,A rich sensor with user-defined data type.,,,
+A.NestedStructSensor,sensor,VehicleDataTypes.TestBranch1.NestedStruct,,,,,A rich sensor with user-defined data type.,,,
+VehicleDataTypes,branch,,,,,,Top-level branch for vehicle data types.,,,
+VehicleDataTypes.TestBranch1,branch,,,,,,Test branch with structs and properties definitions,,,
+VehicleDataTypes.TestBranch1.NestedStruct,struct,,,,,,A struct that is going to be used within another struct - Nested,,,
+VehicleDataTypes.TestBranch1.NestedStruct.x,property,double,,,-10,,x property,,,
+VehicleDataTypes.TestBranch1.NestedStruct.y,property,double,,,,10,y property,,,
+VehicleDataTypes.TestBranch1.NestedStruct.z,property,double,,,,,z property,,,1
+VehicleDataTypes.TestBranch1.ParentStruct,struct,,,,,,A struct that is going to contain properties that are structs themselves,,,
+VehicleDataTypes.TestBranch1.ParentStruct.x_property,property,VehicleDataTypes.TestBranch1.NestedStruct,,,,,A property of struct-type. The struct name is specified relative to the branch,,,
+VehicleDataTypes.TestBranch1.ParentStruct.y_property,property,VehicleDataTypes.TestBranch1.NestedStruct,,,,,A property of struct-type. The struct name is specified as a fully qualified name,,,
+VehicleDataTypes.TestBranch1.ParentStruct.x_properties,property,VehicleDataTypes.TestBranch1.NestedStruct[],,,,,A property of struct-type array and an arraysize,,,
+VehicleDataTypes.TestBranch1.ParentStruct.y_properties,property,VehicleDataTypes.TestBranch1.NestedStruct[],,,,,A property of struct-type array and no arraysize,,,
+VehicleDataTypes.TestBranch1.ParentStruct.z_property,property,double,,,,,A primitive property,,,
diff --git a/tests/vspec/test_structs/expected-signals.csv b/tests/vspec/test_structs/expected-signals.csv
index c709f0ac..0d6d498a 100644
--- a/tests/vspec/test_structs/expected-signals.csv
+++ b/tests/vspec/test_structs/expected-signals.csv
@@ -1,5 +1,5 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.UInt8","sensor","uint8","","km","","","A uint8.","","",""
-"A.ParentStructSensor","sensor","VehicleDataTypes.TestBranch1.ParentStruct","","","","","A rich sensor with user-defined data type.","","",""
-"A.NestedStructSensor","sensor","VehicleDataTypes.TestBranch1.NestedStruct","","","","","A rich sensor with user-defined data type.","","",""
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.UInt8,sensor,uint8,,km,,,A uint8.,,,
+A.ParentStructSensor,sensor,VehicleDataTypes.TestBranch1.ParentStruct,,,,,A rich sensor with user-defined data type.,,,
+A.NestedStructSensor,sensor,VehicleDataTypes.TestBranch1.NestedStruct,,,,,A rich sensor with user-defined data type.,,,
diff --git a/tests/vspec/test_structs/expected.csv b/tests/vspec/test_structs/expected.csv
index e00ea34d..b629d567 100644
--- a/tests/vspec/test_structs/expected.csv
+++ b/tests/vspec/test_structs/expected.csv
@@ -1,13 +1,13 @@
-"Node","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"VehicleDataTypes","branch","","","","","","Top-level branch for vehicle data types.","","",""
-"VehicleDataTypes.TestBranch1","branch","","","","","","Test branch with structs and properties definitions","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct","struct","","","","","","A struct that is going to be used within another struct - Nested","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct.x","property","double","","","-10","","x property","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct.y","property","double","","","","10","y property","","",""
-"VehicleDataTypes.TestBranch1.NestedStruct.z","property","double","","","","","z property","","","1"
-"VehicleDataTypes.TestBranch1.ParentStruct","struct","","","","","","A struct that is going to contain properties that are structs themselves","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.x_property","property","VehicleDataTypes.TestBranch1.NestedStruct","","","","","A property of struct-type. The struct name is specified relative to the branch","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.y_property","property","VehicleDataTypes.TestBranch1.NestedStruct","","","","","A property of struct-type. The struct name is specified as a fully qualified name","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.x_properties","property","VehicleDataTypes.TestBranch1.NestedStruct[]","","","","","A property of struct-type array and an arraysize","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.y_properties","property","VehicleDataTypes.TestBranch1.NestedStruct[]","","","","","A property of struct-type array and no arraysize","","",""
-"VehicleDataTypes.TestBranch1.ParentStruct.z_property","property","double","","","","","A primitive property","","",""
+Node,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+VehicleDataTypes,branch,,,,,,Top-level branch for vehicle data types.,,,
+VehicleDataTypes.TestBranch1,branch,,,,,,Test branch with structs and properties definitions,,,
+VehicleDataTypes.TestBranch1.NestedStruct,struct,,,,,,A struct that is going to be used within another struct - Nested,,,
+VehicleDataTypes.TestBranch1.NestedStruct.x,property,double,,,-10,,x property,,,
+VehicleDataTypes.TestBranch1.NestedStruct.y,property,double,,,,10,y property,,,
+VehicleDataTypes.TestBranch1.NestedStruct.z,property,double,,,,,z property,,,1
+VehicleDataTypes.TestBranch1.ParentStruct,struct,,,,,,A struct that is going to contain properties that are structs themselves,,,
+VehicleDataTypes.TestBranch1.ParentStruct.x_property,property,VehicleDataTypes.TestBranch1.NestedStruct,,,,,A property of struct-type. The struct name is specified relative to the branch,,,
+VehicleDataTypes.TestBranch1.ParentStruct.y_property,property,VehicleDataTypes.TestBranch1.NestedStruct,,,,,A property of struct-type. The struct name is specified as a fully qualified name,,,
+VehicleDataTypes.TestBranch1.ParentStruct.x_properties,property,VehicleDataTypes.TestBranch1.NestedStruct[],,,,,A property of struct-type array and an arraysize,,,
+VehicleDataTypes.TestBranch1.ParentStruct.y_properties,property,VehicleDataTypes.TestBranch1.NestedStruct[],,,,,A property of struct-type array and no arraysize,,,
+VehicleDataTypes.TestBranch1.ParentStruct.z_property,property,double,,,,,A primitive property,,,
diff --git a/tests/vspec/test_structs/test_commandline.py b/tests/vspec/test_structs/test_commandline.py
deleted file mode 100644
index 64230a2c..00000000
--- a/tests/vspec/test_structs/test_commandline.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# Copyright (c) 2022 Contributors to COVESA
-#
-# This program and the accompanying materials are made available under the
-# terms of the Mozilla Public License 2.0 which is available at
-# https://www.mozilla.org/en-US/MPL/2.0/
-#
-# SPDX-License-Identifier: MPL-2.0
-
-import os
-
-
-from pathlib import Path
-import subprocess
-
-HERE = Path(__file__).resolve().parent
-TEST_UNITS = HERE / ".." / "test_units.yaml"
-
-
-def test_error_when_data_types_file_is_missing(tmp_path):
-    otypes = HERE / "output_types_file.json"
-    spec = HERE / "test.vspec"
-    output = tmp_path / "output.json"
-    cmd = f"vspec export json -u {TEST_UNITS} --types-output {otypes} --vspec {spec} --output {output}"
-    env = os.environ.copy()
-    env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env)
-    assert process.returncode != 0
-    assert (
-        "raise ArgumentException"
-        in process.stderr
-    )
diff --git a/tests/vspec/test_structs/test_data_type_parsing.py b/tests/vspec/test_structs/test_data_type_parsing.py
index 65985a05..4d15b3f7 100644
--- a/tests/vspec/test_structs/test_data_type_parsing.py
+++ b/tests/vspec/test_structs/test_data_type_parsing.py
@@ -15,6 +15,7 @@
 HERE = Path(__file__).resolve().parent
 VSS_TOOLS_ROOT = (HERE / ".." / ".." / "..").absolute()
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 @pytest.mark.parametrize(
@@ -64,7 +65,7 @@ def test_data_types_export_single_file(
     cmd = f"vspec export {format}"
     if format == "json":
         cmd += " --pretty"
-    cmd += f" --types {type_file} -u {TEST_UNITS} --vspec {vspec} --output {output}"
+    cmd += f" --types {type_file} -u {TEST_UNITS} -q {TEST_QUANT} --vspec {vspec} --output {output}"
     subprocess.run(cmd.split(), check=True)
     expected_output = HERE / expected_signal
     assert filecmp.cmp(output, expected_output)
@@ -116,7 +117,8 @@ def test_data_types_export_multi_file(
     cmd = f"vspec export {format}"
     if format == "json":
         cmd += " --pretty"
-    cmd += f" --types {type_file} -u {TEST_UNITS} --types-output {types_output} --vspec {vspec} --output {output}"
+    cmd += f" --types {type_file} -u {TEST_UNITS} -q {TEST_QUANT} --types-output {types_output}"
+    cmd += f" --vspec {vspec} --output {output}"
     subprocess.run(cmd.split(), check=True)
     expected_signal = HERE / expected_signal
     expected_data_types = HERE / expected_data_types
@@ -173,7 +175,8 @@ def test_data_types_export_to_proto(
     data_types_out = tmp_path
 
     cmd = (
-        f"vspec export protobuf --types {type_vspec_file} -u {TEST_UNITS} --types-out-dir {data_types_out}"
+        f"vspec export protobuf --types {type_vspec_file} -u {TEST_UNITS} -q {TEST_QUANT}"
+        f" --types-out-dir {data_types_out}"
         f" --vspec {signal_vspec_file} --output {actual_signal_file}"
     )
 
@@ -196,15 +199,15 @@ def test_data_types_export_to_proto(
     [
         (
             "VehicleDataTypesInvalidStructWithQualifiedName.vspec",
-            "2 data type reference errors detected",
+            "'VehicleDataTypes.NestedStruct1' is not a valid datatype",
         ),
         (
             "VehicleDataTypesWithCircularRefs.vspec",
-            "4 data type reference errors detected",
+            "'ParentStruct' is not a valid datatype",
         ),
         (
             "VehicleDataTypesInvalidStruct.vspec",
-            "Data type not found. Data Type: NestedStruct1",
+            "'NestedStruct1' is not a valid datatype",
         ),
     ],
 )
@@ -218,9 +221,11 @@ def test_data_types_invalid_reference_in_data_type_tree(
     output_types = tmp_path / "VehicleDataTypes.vspec"
     vspec = HERE / "test.vspec"
     output = tmp_path / "out.json"
-    cmd = f"vspec export json -u {TEST_UNITS} --pretty --types {types_file}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --types {types_file}"
     cmd += f" --types-output {output_types} --vspec {vspec} --output {output}"
-    process = subprocess.run(cmd.split(), capture_output=True, text=True)
+    env = os.environ.copy()
+    env["COLUMNS"] = "200"
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
     assert error_msg in process.stdout
 
@@ -230,7 +235,7 @@ def test_data_types_invalid_reference_in_data_type_tree(
     [
         (
             "VehicleDataTypesInvalidStructWithOrphanProperties.vspec",
-            "Orphan property detected. y_property is not defined under a struct",
+            "invalid parent: 'VSSDataBranch'",
         )
     ],
 )
@@ -243,12 +248,11 @@ def test_data_types_orphan_properties(types_file, error_msg, tmp_path):
     vspec = HERE / "test.vspec"
     out = tmp_path / "out.json"
 
-    cmd = f"vspec export json -u {TEST_UNITS} --pretty --types {types_file}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --types {types_file}"
     cmd += f" --types-output {types_out} --vspec {vspec} --output {out}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env)
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
     assert error_msg in process.stdout
 
@@ -262,18 +266,14 @@ def test_data_types_invalid_reference_in_signal_tree(tmp_path):
     vspec = HERE / "test-invalid-datatypes.vspec"
     out = tmp_path / "out.json"
 
-    cmd = f"vspec export json -u {TEST_UNITS} --pretty --types {types_file}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --types {types_file}"
     cmd += f" --types-output {types_out} --vspec {vspec} --output {out}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env)
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
 
-    error_msg = (
-        "Following types were referenced in signals but have not been defined: "
-        "VehicleDataTypes.TestBranch1.ParentStruct1, VehicleDataTypes.TestBranch1.NestedStruct1"
-    )
+    error_msg = "'VehicleDataTypes.TestBranch1.ParentStruct1' is not a valid datatype"
     assert error_msg in process.stdout
 
 
@@ -284,17 +284,13 @@ def test_error_when_no_user_defined_data_types_are_provided(tmp_path):
     """
     vspec = HERE / "test.vspec"
     out = tmp_path / "out.json"
-    cmd = f"vspec export json -u {TEST_UNITS} --pretty --vspec {vspec} --output {out}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --vspec {vspec} --output {out}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env)
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
 
-    error_msg = (
-        "Following types were referenced in signals but have not been defined: "
-        "VehicleDataTypes.TestBranch1.ParentStruct, VehicleDataTypes.TestBranch1.NestedStruct"
-    )
+    error_msg = "'VehicleDataTypes.TestBranch1.ParentStruct' is not a valid datatype"
     assert error_msg in process.stdout
 
 
@@ -304,13 +300,17 @@ def test_error_when_no_user_defined_data_types_are_provided(tmp_path):
         (
             "test.vspec",
             "VehicleDataTypesStructWithDataType.vspec",
-            "cannot have datatype, only allowed for signal and property",
+            "Unknown extra attribute: 'VehicleDataTypes.TestBranch1.NestedStruct':'datatype'",
+        ),
+        (
+            "test.vspec",
+            "VehicleDataTypesStructWithUnit.vspec",
+            "Unknown extra attribute: 'VehicleDataTypes.TestBranch1.NestedStruct':'unit'",
         ),
-        ("test.vspec", "VehicleDataTypesStructWithUnit.vspec", "cannot have unit"),
         (
             "test_with_unit_on_struct_signal.vspec",
             "VehicleDataTypes.vspec",
-            "Unit specified for item not using standard datatype",
+            "Cannot use 'unit' with complex datatype: 'VehicleDataTypes.TestBranch1.ParentStruct'",
         ),
     ],
 )
@@ -323,11 +323,12 @@ def test_faulty_use_of_standard_attributes(vspec_file, types_file, error_msg, tm
     vspec_file = HERE / vspec_file
     out = tmp_path / "out.json"
 
-    cmd = f"vspec export json -u {TEST_UNITS} --pretty --types {types_file}"
+    cmd = f"vspec export json -u {TEST_UNITS} -q {TEST_QUANT} --pretty --types {types_file}"
     cmd += f" --types-output {types_out} --vspec {vspec_file} --output {out}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
-    process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, env=env)
+    process = subprocess.run(cmd.split(), capture_output=True, text=True, env=env)
     assert process.returncode != 0
-    assert error_msg in process.stdout
+    print(process.stderr)
+    print(process.stdout)
+    assert error_msg in process.stdout or error_msg in process.stderr
diff --git a/tests/vspec/test_type_error/test_type_error.py b/tests/vspec/test_type_error/test_type_error.py
index c66c86bc..1de8b108 100644
--- a/tests/vspec/test_type_error/test_type_error.py
+++ b/tests/vspec/test_type_error/test_type_error.py
@@ -13,6 +13,7 @@
 HERE = Path(__file__).resolve().parent
 VSS_TOOLS_ROOT = (HERE / ".." / ".." / "..").absolute()
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 @pytest.mark.parametrize(
@@ -31,7 +32,7 @@ def test_description_error(
     vspec_file = HERE / vspec_file
     out = tmp_path / "out.json"
 
-    cmd = f"vspec export json --pretty -u {TEST_UNITS}"
+    cmd = f"vspec export json --pretty -u {TEST_UNITS} -q {TEST_QUANT}"
     if types_file:
         cmd += f" --types {HERE / types_file}"
     if types_out_file:
@@ -42,7 +43,9 @@ def test_description_error(
 
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-    assert "No type specified" in process.stdout
+    print(process.stdout)
+    assert "has 1 model" in process.stdout
+    assert "CRITICAL" in process.stdout
 
 
 @pytest.mark.parametrize(
@@ -66,7 +69,9 @@ def type_case_sensitive(vspec_file: str, tmp_path):
 def test_scope_error(vspec_file: str, tmp_path):
     vspec_file = HERE / vspec_file
     out = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty -u {TEST_UNITS} --vspec {vspec_file} --output {out}"
+    cmd = f"vspec export json --pretty -u {TEST_UNITS}"
+    cmd += f" -q {TEST_QUANT} --vspec {vspec_file} --output {out}"
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     assert process.returncode != 0
-    assert "VSS Node A.UInt8 cannot have children" in process.stdout
+    assert "Invalid nodes=1" in process.stdout
+    assert "A.UInt8.CCC" in process.stdout
diff --git a/tests/vspec/test_types_with_uuid/expected_no_uuid.csv b/tests/vspec/test_types_with_uuid/expected_no_uuid.csv
index 375f21b5..abdec4d4 100644
--- a/tests/vspec/test_types_with_uuid/expected_no_uuid.csv
+++ b/tests/vspec/test_types_with_uuid/expected_no_uuid.csv
@@ -1,5 +1,5 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default"
-"A","branch","","","","","","Branch A.","","",""
-"A.Sensor","sensor","float","","km","","","A sensor.","","",""
-"A.Attribute","sensor","float","","km","","","An attribute.","","",""
-"A.Actuator","sensor","float","","km","","","An actuator.","","",""
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default
+A,branch,,,,,,Branch A.,,,
+A.Sensor,sensor,float,,km,,,A sensor.,,,
+A.Attribute,sensor,float,,km,,,An attribute.,,,
+A.Actuator,sensor,float,,km,,,An actuator.,,,
diff --git a/tests/vspec/test_types_with_uuid/expected_uuid.csv b/tests/vspec/test_types_with_uuid/expected_uuid.csv
index ba0d2f5e..9722a8fb 100644
--- a/tests/vspec/test_types_with_uuid/expected_uuid.csv
+++ b/tests/vspec/test_types_with_uuid/expected_uuid.csv
@@ -1,5 +1,5 @@
-"Signal","Type","DataType","Deprecated","Unit","Min","Max","Desc","Comment","Allowed","Default","Id"
-"A","branch","","","","","","Branch A.","","","","6342f7c4796a5f78a93bff040a974d02"
-"A.Sensor","sensor","float","","km","","","A sensor.","","","","540af2eb86b450ae814011d2746cece0"
-"A.Attribute","sensor","float","","km","","","An attribute.","","","","ddba2946fd215f2f8f869c793e2aff08"
-"A.Actuator","sensor","float","","km","","","An actuator.","","","","2c9f66c9e98c5023ab222fd15c90ed22"
+Signal,Type,DataType,Deprecated,Unit,Min,Max,Desc,Comment,Allowed,Default,Id
+A,branch,,,,,,Branch A.,,,,6342f7c4796a5f78a93bff040a974d02
+A.Sensor,sensor,float,,km,,,A sensor.,,,,540af2eb86b450ae814011d2746cece0
+A.Attribute,sensor,float,,km,,,An attribute.,,,,ddba2946fd215f2f8f869c793e2aff08
+A.Actuator,sensor,float,,km,,,An actuator.,,,,2c9f66c9e98c5023ab222fd15c90ed22
diff --git a/tests/vspec/test_types_with_uuid/test_uuid.py b/tests/vspec/test_types_with_uuid/test_uuid.py
index a18432b7..8bdf426c 100644
--- a/tests/vspec/test_types_with_uuid/test_uuid.py
+++ b/tests/vspec/test_types_with_uuid/test_uuid.py
@@ -12,12 +12,13 @@
 
 HERE = Path(__file__).resolve().parent
 TEST_UNITS = HERE / ".." / "test_units.yaml"
+TEST_QUANT = HERE / ".." / "test_quantities.yaml"
 
 
 def run_exporter(exporter, argument, compare_suffix, tmp_path):
     vspec = HERE / "test.vspec"
     out = tmp_path / f"out.{exporter}"
-    cmd = f"vspec export {exporter}{argument} -u {TEST_UNITS} --vspec {vspec} --output {out}"
+    cmd = f"vspec export {exporter}{argument} -u {TEST_UNITS} -q {TEST_QUANT} --vspec {vspec} --output {out}"
     subprocess.run(cmd.split(), check=True)
     expected = HERE / f"expected_{compare_suffix}.{exporter}"
     assert filecmp.cmp(out, expected)
@@ -36,7 +37,7 @@ def test_uuid(tmp_path):
 def run_error_test(tool, argument, arg_error_expected: bool, tmp_path):
     vspec = HERE / "test.vspec"
     out = tmp_path / "out.json"
-    cmd = f"{tool} {argument} -u {TEST_UNITS} --vspec {vspec} --output {out}"
+    cmd = f"{tool} {argument} -u {TEST_UNITS} -q {TEST_QUANT} --vspec {vspec} --output {out}"
     process = subprocess.run(cmd.split(), capture_output=True, text=True)
     if arg_error_expected:
         assert process.returncode != 0
diff --git a/tests/vspec/test_units.yaml b/tests/vspec/test_units.yaml
index fd685725..52629a9a 100644
--- a/tests/vspec/test_units.yaml
+++ b/tests/vspec/test_units.yaml
@@ -11,22 +11,27 @@ celsius:
   quantity: temperature
   allowed-datatypes: ['numeric']
 mm:
-  label: millimeter
-  description: Distance measured in millimeters
-  domain: distance
+  unit: millimeter
+  definition: Distance measured in millimeters
+  quantity: distance
+  allowed-datatypes: ['numeric']
 m:
-  label: meter
-  description: Distance measured in meters
-  domain: distance
+  unit: meter
+  definition: Distance measured in meters
+  quantity: distance
+  allowed-datatypes: ['numeric']
 degrees/s:
-  label: degree per second
-  description: Angular speed measured in degrees per second
-  domain: angular speed
+  unit: degree per second
+  definition: Angular speed measured in degrees per second
+  quantity: angular-speed
+  allowed-datatypes: ['numeric']
 rpm:
-  label: revolutions per minute
-  description: Rotational speed measured in revolutions per minute
-  domain: rotational speed
+  unit: revolutions per minute
+  definition: Rotational speed measured in revolutions per minute
+  quantity: rotational-speed
+  allowed-datatypes: ['numeric']
 percent:
-  label: percent
-  description: Relation measured in percent
-  domain: relation
+  unit: percent
+  definition: Relation measured in percent
+  quantity: relation
+  allowed-datatypes: ['numeric']
diff --git a/tests/vspec/test_units/test_units.py b/tests/vspec/test_units/test_units.py
index 5eee522d..14b117d6 100644
--- a/tests/vspec/test_units/test_units.py
+++ b/tests/vspec/test_units/test_units.py
@@ -23,6 +23,7 @@ def run_unit(
     quantities=None,
     grep_present: bool = True,
     grep_string: Optional[str] = None,
+    fails: bool = False,
 ):
     if not quantities:
         quantities = []
@@ -32,25 +33,26 @@ def run_unit(
     out = tmp_path / "out.json"
     unit_argument = " ".join([f"-u {HERE / unit}" for unit in units])
     quantity_argument = " ".join([f"-q {HERE / quantity}" for quantity in quantities])
-    cmd = f"vspec export json --pretty --vspec {spec} {unit_argument} {quantity_argument} --output {out}"
+    cmd = f"vspec export json --pretty --vspec {spec}"
+    cmd += f" {unit_argument} {quantity_argument} --output {out}"
     env = os.environ.copy()
     # Long line needed as file name printed in some error messages
     env["COLUMNS"] = "200"
     process = subprocess.run(
         cmd.split(), capture_output=True, text=True, cwd=HERE, env=env
     )
-    if process.returncode != 0:
-        pass
-    assert process.returncode == 0
-    assert filecmp.cmp(HERE / expected_file, out)
+
+    if fails:
+        assert process.returncode != 0
+    else:
+        assert process.returncode == 0
+        assert filecmp.cmp(HERE / expected_file, out)
 
     if grep_present and grep_string:
-        assert grep_string in process.stdout
+        assert grep_string in process.stdout or grep_string in process.stderr
 
 
-def run_unit_error(
-    tmp_path, vspec_file, units, grep_error, quantities=None
-):
+def run_unit_error(tmp_path, vspec_file, units, grep_error, quantities=None):
     if not quantities:
         quantities = []
     if not units:
@@ -58,12 +60,14 @@ def run_unit_error(
     out = tmp_path / "out.json"
     unit_argument = " ".join([f"-u {HERE / unit}" for unit in units])
     quantity_argument = " ".join([f"-q {HERE / quantity}" for quantity in quantities])
-    cmd = f"vspec export json --pretty --vspec {vspec_file} {unit_argument} {quantity_argument} --output {out}"
+    cmd = f"vspec export json --pretty --vspec {vspec_file}"
+    cmd += f" {unit_argument} {quantity_argument} --output {out}"
     env = os.environ.copy()
     # Long line needed as file name printed in some error messages
     env["COLUMNS"] = "300"
     process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, cwd=HERE, env=env)
+        cmd.split(), capture_output=True, text=True, cwd=HERE, env=env
+    )
     assert process.returncode != 0
     if grep_error:
         assert grep_error in process.stdout or grep_error in process.stderr
@@ -95,6 +99,10 @@ def test_multiple_duplication(tmp_path):
         "signals_with_special_units.vspec",
         ["units_all.yaml", "units_all.yaml"],
         "expected_special.json",
+        None,
+        True,
+        "redefinition of 'puncheon'",
+        True,
     )
 
 
@@ -126,8 +134,9 @@ def test_multiple_unit_files(tmp_path):
 
 
 def test_unit_error_no_unit_file(tmp_path):
-    run_unit_error(tmp_path, "signals_with_special_units.vspec",
-                   None, "No units defined")
+    run_unit_error(
+        tmp_path, "signals_with_special_units.vspec", None, "No 'unit' files defined"
+    )
 
 
 # Not all units defined
@@ -138,7 +147,7 @@ def test_unit_error_unit_file_incomplete(tmp_path):
         tmp_path,
         "signals_with_special_units.vspec",
         ["units_hogshead.yaml"],
-        "Unknown unit",
+        "'puncheon' is not a valid unit",
     )
 
 
@@ -154,12 +163,6 @@ def test_unit_error_missing_file(tmp_path):
     )
 
 
-def test_unit_on_branch(tmp_path):
-    run_unit_error(
-        tmp_path, "unit_on_branch.vspec", ["units_all.yaml"], "cannot have unit"
-    )
-
-
 # Quantity tests
 
 
@@ -210,7 +213,8 @@ def test_explicit_quantity_warning(tmp_path):
         "expected_special.json",
         ["quantity_volym.yaml"],
         True,
-        "Quantity volume used by unit puncheon has not been defined",
+        "Invalid quantity: 'volume'",
+        True,
     )
 
 
@@ -222,7 +226,8 @@ def test_quantity_redefinition(tmp_path):
         "expected_special.json",
         ["quantity_volym.yaml", "quantity_volym.yaml"],
         True,
-        "Redefinition of quantity volym",
+        "redefinition of 'volym'",
+        True,
     )
 
 
@@ -234,6 +239,6 @@ def test_quantity_err_no_def(tmp_path):
         tmp_path,
         "signals_with_special_units.vspec",
         ["units_all.yaml"],
-        "No definition found for quantity volume",
+        "'volume' is 'None'",
         ["quantities_no_def.yaml"],
     )
diff --git a/tests/vspec/test_units/unit_on_branch.vspec b/tests/vspec/test_units/unit_on_branch.vspec
deleted file mode 100644
index 92fc7c14..00000000
--- a/tests/vspec/test_units/unit_on_branch.vspec
+++ /dev/null
@@ -1,10 +0,0 @@
-A:
-  type: branch
-  description: Specifying unit on branch is not allowed!
-  unit: hogshead
-
-A.UInt8:
-  datatype: uint8
-  type: sensor
-  unit: hogshead
-  description: This description is mandatory!
diff --git a/tests/vspec/test_units/units_all.yaml b/tests/vspec/test_units/units_all.yaml
index dfec3dac..18607fd9 100644
--- a/tests/vspec/test_units/units_all.yaml
+++ b/tests/vspec/test_units/units_all.yaml
@@ -1,9 +1,10 @@
-units:
-  puncheon:
-    label: Puncheon
-    description: Volume measure in puncheons (1 puncheon = 318 liters)
-    domain: volume
-  hogshead:
-    label: Hogshead
-    description: Volume measure in hogsheads (1 hogshead = 238 liters)
-    domain: volume
\ No newline at end of file
+puncheon:
+  unit: Puncheon
+  definition: Volume measure in puncheons (1 puncheon = 318 liters)
+  quantity: volume
+  allowed-datatypes: [numeric]
+hogshead:
+  unit: Hogshead
+  definition: Volume measure in hogsheads (1 hogshead = 238 liters)
+  quantity: volume
+  allowed-datatypes: [numeric]
diff --git a/tests/vspec/test_units/units_hogshead.yaml b/tests/vspec/test_units/units_hogshead.yaml
index 5359c435..1d09991a 100644
--- a/tests/vspec/test_units/units_hogshead.yaml
+++ b/tests/vspec/test_units/units_hogshead.yaml
@@ -1,5 +1,5 @@
-units:
-  hogshead:
-    label: Hogshead
-    description: Volume measure in hogsheads (1 hogshead = 238 liters)
-    domain: volume
+hogshead:
+  unit: Hogshead
+  definition: Volume measure in hogsheads (1 hogshead = 238 liters)
+  quantity: volume
+  allowed-datatypes: [numeric]
diff --git a/tests/vspec/test_units/units_puncheon.yaml b/tests/vspec/test_units/units_puncheon.yaml
index deb13956..eb44d97b 100644
--- a/tests/vspec/test_units/units_puncheon.yaml
+++ b/tests/vspec/test_units/units_puncheon.yaml
@@ -1,5 +1,5 @@
-units:
-  puncheon:
-    label: Puncheon
-    description: Volume measure in puncheons (1 puncheon = 318 liters)
-    domain: volume
+puncheon:
+  unit: Puncheon
+  definition: Volume measure in puncheons (1 puncheon = 318 liters)
+  quantity: volume
+  allowed-datatypes: [numeric]
diff --git a/tests/vspec/test_units_no_quantity/test_units_no_quantity.py b/tests/vspec/test_units_no_quantity/test_units_no_quantity.py
index f2a6cb6d..2adeac63 100644
--- a/tests/vspec/test_units_no_quantity/test_units_no_quantity.py
+++ b/tests/vspec/test_units_no_quantity/test_units_no_quantity.py
@@ -23,14 +23,22 @@ def run_unit(
     quantity_argument="",
     grep_present: bool = True,
     grep_string: Optional[str] = None,
+    fails: bool = False,
 ):
     out = tmp_path / "out.json"
-    cmd = f"vspec export json --pretty --vspec {vspec_file} {unit_argument} {quantity_argument} --output {out}"
+    cmd = f"vspec export json --pretty --vspec {vspec_file}"
+    cmd += f" {unit_argument} {quantity_argument} --output {out}"
     env = os.environ.copy()
     env["COLUMNS"] = "200"
     process = subprocess.run(
-        cmd.split(), capture_output=True, text=True, cwd=HERE, env=env, check=True)
-    assert filecmp.cmp(out, HERE / expected_file)
+        cmd.split(), capture_output=True, text=True, cwd=HERE, env=env
+    )
+    print(process.stdout)
+    if fails:
+        assert process.returncode != 0
+    else:
+        assert process.returncode == 0
+        assert filecmp.cmp(out, HERE / expected_file)
 
     if grep_string and grep_present:
         assert grep_string in process.stdout or grep_string in process.stderr
@@ -44,28 +52,21 @@ def test_default_unit_no_quantity_warning(tmp_path):
         tmp_path,
         "test.vspec",
         "-u ../test_units.yaml",
-        "expected.json",
+        None,
         "",
         True,
-        "No quantities defined",
-    )
-    run_unit(
-        tmp_path,
-        "test.vspec",
-        "-u ../test_units.yaml",
-        "expected.json",
-        "",
-        False,
-        "Quantity length used by unit km has not been defined",
+        "No 'quantity' files defined.",
+        True,
     )
     run_unit(
         tmp_path,
         "test.vspec",
         "-u ../test_units.yaml",
-        "expected.json",
+        None,
         "",
         False,
-        "Quantity temperature used by unit celsius has not been defined",
+        "Invalid quantity: 'length'",
+        True,
     )
 
 
@@ -77,28 +78,11 @@ def test_default_unit_quantities_missing(tmp_path):
         tmp_path,
         "test.vspec",
         "-u ../test_units.yaml",
-        "expected.json",
+        None,
         "-q test_quantities.yaml",
         False,
-        "No quantities defined",
-    )
-    run_unit(
-        tmp_path,
-        "test.vspec",
-        "-u ../test_units.yaml",
-        "expected.json",
-        "-q test_quantities.yaml",
+        "Invalid quantity: 'length'",
         True,
-        "Quantity length used by unit km has not been defined",
-    )
-    run_unit(
-        tmp_path,
-        "test.vspec",
-        "-u ../test_units.yaml",
-        "expected.json",
-        "-q test_quantities.yaml",
-        True,
-        "Quantity temperature used by unit celsius has not been defined",
     )