Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
4413c62
server: Implement the read-only `$value` routes
thammel Aug 31, 2026
f9f9f88
Merge branch 'feat/446-get-value-endpoints' into feat/serialization-f…
hpoeche Oct 9, 2026
a1480a0
Integrate /$value endpoint tests with existing tests
hpoeche Oct 9, 2026
bff5dc7
Rework ValueOnly serialization as `JSONEncoder` subclass
hpoeche Oct 10, 2026
00016d9
Rewire path serialization into JSON Encoder subclass
JAB1305 Oct 10, 2026
2aec044
Draft central modifier and encoder handling
JAB1305 Oct 11, 2026
54a49f4
Exmplatory wire up for get submodel endpoint
JAB1305 Oct 11, 2026
fde20f9
Implement Reference-Serialization as AASToJsonEnocoder subclass
hpoeche Oct 14, 2026
39d4685
Implement Metadata-Serialization as AASToJsonEnocoder subclass
hpoeche Oct 14, 2026
bd0dbcd
Fix import errors and missing copyright
hpoeche Oct 14, 2026
2fe15bc
Add testing for path serialization
hpoeche Oct 14, 2026
ca6a363
Refactor bool related names for better clarity
JAB1305 Oct 14, 2026
1e45688
Adjust `deep` as init arg on path encoder
JAB1305 Oct 19, 2026
d6dbf01
Refine `PathJsonEncoder` testing
hpoeche Oct 23, 2026
c04a436
Implement modifier `level=core` for Normal and ValueOnly
hpoeche Oct 25, 2026
1d10056
Refactor adapter structure
JAB1305 Oct 27, 2026
08fc93b
Restructure imports
JAB1305 Oct 27, 2026
c57c2cb
Restrurcture tests following serializers
JAB1305 Oct 27, 2026
b92fd60
Create metadata serialization test
JAB1305 Oct 27, 2026
05f8eb4
Implement modifier `extent=WithoutBLOBValue`
hpoeche Oct 27, 2026
7321c0c
Realign path serialization and tests with supported types
JAB1305 Oct 27, 2026
93b98cf
Merge branch 'feat/serialization-formats' of https://github.com/rwth-…
JAB1305 Oct 27, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions sdk/basyx/aas/adapter/json/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@
:class:`ObjectStore <basyx.aas.model.provider.AbstractObjectStore>`.
"""

from .formats.metadata_serialization import MetadataJsonEncoder
from .formats.path_serialization import PathJsonEncoder
from .formats.reference_serialization import ReferenceJsonEncoder
from .formats.value_only_serialization import ValueOnlyJsonEncoder
from .json_deserialization import (
AASFromJsonDecoder,
StrictAASFromJsonDecoder,
Expand All @@ -43,4 +47,8 @@
"StrippedAASToJsonEncoder",
"object_store_to_json",
"write_aas_json_file",
"PathJsonEncoder",
"ValueOnlyJsonEncoder",
"MetadataJsonEncoder",
"ReferenceJsonEncoder",
]
129 changes: 129 additions & 0 deletions sdk/basyx/aas/adapter/json/formats/metadata_serialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# Copyright (c) 2026 the Eclipse BaSyx Authors
#
# This program and the accompanying materials are made available under the terms of the MIT License, available in
# the LICENSE file of this project.
#
# SPDX-License-Identifier: MIT
from typing import Dict

from basyx.aas import model

from ..json_serialization import AASToJsonEncoder


class MetadataJsonEncoder(AASToJsonEncoder):
"""
Custom JSONEncoder that serializes BaSyx Python SDK objects into the JSON format "Metadata". This equals
the normal serialization format but omits certain keys.
"""

@classmethod
def _asset_administration_shell_to_json(
cls, obj: model.AssetAdministrationShell
) -> Dict[str, object]:
data = super()._asset_administration_shell_to_json(obj)
data.pop("assetInformation", None)
data.pop("submodels", None)
return data

@classmethod
def _submodel_to_json(
cls, obj: model.Submodel
) -> Dict[str, object]:
data = super()._submodel_to_json(obj)
data.pop("submodelElements", None)
return data

@classmethod
def _submodel_element_collection_to_json(
cls, obj: model.SubmodelElementCollection
) -> Dict[str, object]:
data = super()._submodel_element_collection_to_json(obj)
data.pop("value", None)
return data

@classmethod
def _submodel_element_list_to_json(
cls, obj: model.SubmodelElementList
) -> Dict[str, object]:
data = super()._submodel_element_list_to_json(obj)
data.pop("value", None)
return data

@classmethod
def _entity_to_json(cls, obj: model.Entity) -> Dict[str, object]:
data = super()._entity_to_json(obj)
data.pop("statements", None)
data.pop("globalAssetId", None)
data.pop("specificAssetIds", None)
return data

@classmethod
def _basic_event_element_to_json(
cls, obj: model.BasicEventElement
) -> Dict[str, object]:
data = super()._basic_event_element_to_json(obj)
data.pop("observed", None)
return data

@classmethod
def _property_to_json(cls, obj: model.Property) -> Dict[str, object]:
data = super()._property_to_json(obj)
data.pop("value", None)
data.pop("valueId", None)
return data

@classmethod
def _multi_language_property_to_json(cls, obj: model.MultiLanguageProperty) -> Dict[str, object]:
data = super()._multi_language_property_to_json(obj)
data.pop("value", None)
data.pop("valueId", None)
return data

@classmethod
def _range_to_json(cls, obj: model.Range) -> Dict[str, object]:
data = super()._range_to_json(obj)
data.pop("min", None)
data.pop("max", None)
return data

@classmethod
def _reference_element_to_json(
cls, obj: model.ReferenceElement
) -> Dict[str, object]:
data = super()._reference_element_to_json(obj)
data.pop("value", None)
return data

@classmethod
def _relationship_element_to_json(
cls, obj: model.RelationshipElement
) -> Dict[str, object]:
data = super()._relationship_element_to_json(obj)
data.pop("first", None)
data.pop("second", None)
return data

@classmethod
def _annotated_relationship_element_to_json(
cls, obj: model.AnnotatedRelationshipElement
) -> Dict[str, object]:
data = super()._annotated_relationship_element_to_json(obj)
data.pop("first", None)
data.pop("second", None)
data.pop("annotations", None)
return data

@classmethod
def _blob_to_json(cls, obj: model.Blob) -> Dict[str, object]:
data = super()._blob_to_json(obj)
data.pop("value", None)
data.pop("contentType", None)
return data

@classmethod
def _file_to_json(cls, obj: model.File) -> Dict[str, object]:
data = super()._file_to_json(obj)
data.pop("value", None)
data.pop("contentType", None)
return data
104 changes: 104 additions & 0 deletions sdk/basyx/aas/adapter/json/formats/path_serialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
# Copyright (c) 2026 the Eclipse BaSyx Authors
#
# This program and the accompanying materials are made available under the terms of the MIT License, available in
# the LICENSE file of this project.
#
# SPDX-License-Identifier: MIT

from typing import Iterable, List, Tuple

from basyx.aas import model

from ..json_serialization import AASToJsonEncoder


# Types for which the $path content modifier returns the idShort of the requested element and a list
# of idShort paths to its child elements (AAS API Part 2).
_PATH_SUPPORTED_TYPES: Tuple[type, ...] = (
model.Submodel,
model.SubmodelElementCollection,
model.SubmodelElementList,
model.AnnotatedRelationshipElement,
model.Entity,
)


class PathJsonEncoder(AASToJsonEncoder):
"""
Custom JSONEncoder that serializes selected AAS objects as the list of idShortPaths reachable
from them (``$path`` content modifier). Supported inputs are :class:`~basyx.aas.model.Submodel`,
:class:`~basyx.aas.model.SubmodelElementCollection`, :class:`~basyx.aas.model.SubmodelElementList`,
:class:`~basyx.aas.model.AnnotatedRelationshipElement`, and :class:`~basyx.aas.model.Entity`.
Any other type raises :class:`TypeError`.

:param deep: If True (default), all descendants on all sublevels are included. If False, only
the requested element and its direct children are included. Compare to
SerializationModifier level.
"""

def __init__(self, *args, deep: bool = True, **kwargs):
super().__init__(*args, **kwargs)
self.deep = deep

def default(self, obj: object) -> object:
if isinstance(obj, _PATH_SUPPORTED_TYPES):
return self._referable_to_path(obj)
raise TypeError(
f"Object of type {type(obj).__name__} has no $path representation; "
f"supported types are: {', '.join(t.__name__ for t in _PATH_SUPPORTED_TYPES)}."
)

def _referable_to_path(self, obj: model.Referable) -> List[str]:
if isinstance(obj, model.Submodel):
# Submodel is Identifiable; its idShort is optional and never part of an idShortPath.
paths: List[str] = []
self._append_children(paths, obj.submodel_element, prefix=[])
return paths
# For the remaining supported types the idShort of the requested element is the root of
# every returned idShortPath. If it is missing, no path can be computed from this element.
if obj.id_short is None:
raise ValueError(f"{obj!r} has no idShort, so no idShortPath can be computed for it!")
prefix = [obj.id_short]
paths = [model.Referable.build_id_short_path(prefix)]
if isinstance(obj, model.SubmodelElementList):
self._append_indexed_children(paths, obj.value, prefix)
elif isinstance(obj, model.SubmodelElementCollection):
self._append_children(paths, obj.value, prefix)
elif isinstance(obj, model.Entity):
self._append_children(paths, obj.statement, prefix)
elif isinstance(obj, model.AnnotatedRelationshipElement):
self._append_children(paths, obj.annotation, prefix)
return paths

def _append_children(
self, paths: List[str], children: Iterable[model.SubmodelElement], prefix: List[str]
) -> None:
for child in children:
if child.id_short is None:
raise ValueError(
f"{child!r} has no idShort, so no idShortPath can be computed for it!"
)
child_prefix = prefix + [child.id_short]
paths.append(model.Referable.build_id_short_path(child_prefix))
if self.deep:
self._recurse(paths, child, child_prefix)

def _append_indexed_children(
self, paths: List[str], children: Iterable[model.SubmodelElement], prefix: List[str]
) -> None:
for index, child in enumerate(children):
child_prefix = prefix + [str(index)]
paths.append(model.Referable.build_id_short_path(child_prefix))
if self.deep:
self._recurse(paths, child, child_prefix)

def _recurse(self, paths: List[str], element: object, prefix: List[str]) -> None:
if isinstance(element, model.SubmodelElementList):
self._append_indexed_children(paths, element.value, prefix)
elif isinstance(element, model.SubmodelElementCollection):
self._append_children(paths, element.value, prefix)
elif isinstance(element, model.Entity):
self._append_children(paths, element.statement, prefix)
elif isinstance(element, model.AnnotatedRelationshipElement):
self._append_children(paths, element.annotation, prefix)
# Leaf elements have no children to traverse into.
42 changes: 42 additions & 0 deletions sdk/basyx/aas/adapter/json/formats/reference_serialization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Copyright (c) 2026 the Eclipse BaSyx Authors
#
# This program and the accompanying materials are made available under the terms of the MIT License, available in
# the LICENSE file of this project.
#
# SPDX-License-Identifier: MIT
from typing import Callable, Dict, Type

from basyx.aas import model

from ..json_serialization import AASToJsonEncoder


class ReferenceJsonEncoder(AASToJsonEncoder):
"""
Custom JSONEncoder that serializes :class:`~basyx.aas.model.Referable` objects, which are contained
in an :class:`~basyx.aas.model.Identifiable`, as their :class:`~basyx.aas.model.ModelReference`.
"""

@classmethod
def _get_aas_class_serializers(cls) -> Dict[Type, Callable]:
normal_serializers = super()._get_aas_class_serializers()
serializers: Dict[Type, Callable] = {}
for typ in normal_serializers:
if not issubclass(typ, model.Referable):
serializers[typ] = cls._no_referable

serializers.update({
model.Reference: normal_serializers[model.Reference],
model.Key: normal_serializers[model.Key],
model.Referable: cls._referable_to_reference,
})

return serializers

@classmethod
def _referable_to_reference(cls, obj: model.Referable) -> model.Reference:
return model.ModelReference.from_referable(obj)

@classmethod
def _no_referable(cls, obj: object):
raise TypeError(f"Object of type {obj.__class__.__name__} has no Reference serialization")
Loading
Loading