Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
873c00b
Fail Metadata serialization on unsupported types
hpoeche Oct 27, 2026
4bb8e0f
Fix docstrings
hpoeche Oct 27, 2026
2ea9b55
Implement constant order in JSON serialization
hpoeche Oct 28, 2026
3c43a08
Make `modelType` first key in serialization of `model.Referable`s
hpoeche Oct 29, 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
103 changes: 103 additions & 0 deletions sdk/basyx/aas/adapter/_ordering.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# 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 Optional, Tuple

from basyx.aas import model

from . import _generic


def reference_sort_key(obj: Optional[model.Reference]) -> Tuple:
"""
Sort key of a Reference, consisting of its type, its keys and its referredSemanticId

:param obj: object of class Reference or ``None``
:return: tuple which totally orders References, ``()`` for ``None``
"""
if obj is None:
return ()
return (
_generic.REFERENCE_TYPES[obj.__class__],
tuple((_generic.KEY_TYPES[key.type], key.value) for key in obj.key),
reference_sort_key(obj.referred_semantic_id),
)


def specific_asset_id_sort_key(obj: model.SpecificAssetId) -> Tuple:
"""
Sort key of a SpecificAssetId, consisting of all its attributes, since its name is not unique

:param obj: object of class SpecificAssetId
:return: tuple which totally orders SpecificAssetIds
"""
return (
obj.name,
obj.value,
reference_sort_key(obj.external_subject_id),
reference_sort_key(obj.semantic_id),
tuple(sorted(reference_sort_key(ref) for ref in obj.supplemental_semantic_id)),
)


def embedded_data_specification_sort_key(obj: model.EmbeddedDataSpecification) -> Tuple:
"""
Sort key of an EmbeddedDataSpecification, consisting of its dataSpecification

:param obj: object of class EmbeddedDataSpecification
:return: tuple which totally orders EmbeddedDataSpecifications
"""
return reference_sort_key(obj.data_specification)


def value_reference_pair_sort_key(obj: model.ValueReferencePair) -> Tuple:
"""
Sort key of a ValueReferencePair, consisting of its value and its valueId, since its value is not unique

:param obj: object of class ValueReferencePair
:return: tuple which totally orders ValueReferencePairs
"""
return model.datatypes.xsd_repr(obj.value), reference_sort_key(obj.value_id)


def extension_sort_key(obj: model.Extension) -> str:
"""
Sort key of an Extension, its name, which is unique (Constraint AASd-077)

:param obj: object of class Extension
:return: name of the Extension
"""
return obj.name


def qualifier_sort_key(obj: model.Qualifier) -> str:
"""
Sort key of a Qualifier, its type, which is unique (Constraint AASd-021)

:param obj: object of class Qualifier
:return: type of the Qualifier
"""
return obj.type


def referable_sort_key(obj: model.Referable) -> str:
"""
Sort key of a Referable within its namespace, its idShort, which is unique (Constraint AASd-022)

:param obj: object of class Referable
:return: idShort of the Referable, ``""`` if it has none
"""
return obj.id_short or ""


def identifiable_sort_key(obj: model.Identifiable) -> str:
"""
Sort key of an Identifiable, its globally unique id

:param obj: object of class Identifiable
:return: id of the Identifiable
"""
return obj.id
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",
]
140 changes: 140 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,140 @@
# 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 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 _get_aas_class_serializers(cls) -> Dict[Type, Callable]:
mapping = super()._get_aas_class_serializers()

mapping.pop(model.AssetInformation, None)
mapping.pop(model.Resource, None)
mapping.pop(model.SpecificAssetId, None)
mapping.pop(model.ConceptDescription, None)

return mapping

@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
108 changes: 108 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,108 @@
# 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 basyx.aas.adapter import _ordering

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.Submodel`,
:class:`~basyx.aas.model.submodel.SubmodelElementCollection`,
:class:`~basyx.aas.model.submodel.SubmodelElementList`,
:class:`~basyx.aas.model.submodel.AnnotatedRelationshipElement`, and :class:`~basyx.aas.model.submodel.Entity`.
Any other type raises :class:`TypeError`.

The idShortPaths of children are listed in a canonical order: Elements of a SubmodelElementList in the order of
the list, all other children sorted by their idShort.

: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 sorted(children, key=_ordering.referable_sort_key):
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.
Loading
Loading