@@ -55,8 +55,42 @@ class AASToJsonEncoder(json.JSONEncoder):
5555 :cvar stripped: If True, the JSON objects will be serialized in a stripped manner, excluding some attributes.
5656 Defaults to ``False``.
5757 See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91
58+ :cvar sort_arrays: If True, JSON arrays that originate from unordered Python sets are sorted by a stable key, so
59+ that the serialized output is deterministic across runs (Python sets have non-deterministic
60+ iteration order due to hash randomization). Defaults to ``False`` to preserve backward
61+ compatibility. Enabled via the ``sort_arrays`` parameter of :func:`write_aas_json_file` or
62+ :func:`object_store_to_json`.
5863 """
5964 stripped = False
65+ sort_arrays = False
66+
67+ @classmethod
68+ def _maybe_sort (cls , items : Iterable , key : Callable ) -> list :
69+ """
70+ Return ``items`` as a list, sorted by ``key`` only if :attr:`sort_arrays` is enabled.
71+
72+ This is used for JSON arrays that originate from unordered Python sets. Sorting makes the serialized
73+ output deterministic across runs; it is opt-in so that the default behavior remains unchanged.
74+
75+ :param items: The iterable (typically a set) to convert to a list.
76+ :param key: Sort key callable, applied to each item when sorting is enabled.
77+ :return: A list of the items, sorted iff :attr:`sort_arrays` is True.
78+ """
79+ if cls .sort_arrays :
80+ return sorted (items , key = key )
81+ return list (items )
82+
83+ @staticmethod
84+ def _reference_sort_key (ref : model .Reference ) -> list :
85+ """
86+ Stable sort key for a :class:`~basyx.aas.model.base.Reference`, derived from its structural ``key`` chain
87+ rather than from ``str()``/``repr()``. This keeps the serialized order of set-valued reference attributes
88+ (e.g. ``submodels``, ``isCaseOf``) independent of any future changes to the ``__repr__``/``__str__`` methods.
89+
90+ :param ref: The reference to derive a sort key for.
91+ :return: A list of ``(key type name, key value)`` tuples, comparable across references.
92+ """
93+ return [(k .type .name , k .value ) for k in ref .key ]
6094
6195 @classmethod
6296 def _get_aas_class_serializers (cls ) -> Dict [Type , Callable ]:
@@ -335,7 +369,7 @@ def _concept_description_to_json(cls, obj: model.ConceptDescription) -> Dict[str
335369 """
336370 data = cls ._abstract_classes_to_json (obj )
337371 if obj .is_case_of :
338- data ['isCaseOf' ] = list (obj .is_case_of )
372+ data ['isCaseOf' ] = cls . _maybe_sort (obj .is_case_of , key = cls . _reference_sort_key )
339373 return data
340374
341375 @classmethod
@@ -390,7 +424,7 @@ def _asset_administration_shell_to_json(cls, obj: model.AssetAdministrationShell
390424 if obj .asset_information :
391425 data ["assetInformation" ] = obj .asset_information
392426 if not cls .stripped and obj .submodel :
393- data ["submodels" ] = list (obj .submodel )
427+ data ["submodels" ] = cls . _maybe_sort (obj .submodel , key = cls . _reference_sort_key )
394428 return data
395429
396430 # #################################################################
@@ -687,22 +721,29 @@ class StrippedAASToJsonEncoder(AASToJsonEncoder):
687721 stripped = True
688722
689723
690- def _select_encoder (stripped : bool , encoder : Optional [Type [AASToJsonEncoder ]] = None ) -> Type [AASToJsonEncoder ]:
724+ def _select_encoder (stripped : bool , encoder : Optional [Type [AASToJsonEncoder ]] = None ,
725+ sort_arrays : bool = False ) -> Type [AASToJsonEncoder ]:
691726 """
692727 Returns the correct encoder based on the stripped parameter. If an encoder class is given, stripped is ignored.
693728
694729 :param stripped: If true, an encoder for parsing stripped JSON objects is selected. Ignored if an encoder class is
695730 specified.
696731 :param encoder: Is returned, if specified.
732+ :param sort_arrays: If true, a subclass of the selected encoder with :attr:`~.AASToJsonEncoder.sort_arrays`
733+ enabled is returned, so that arrays originating from unordered sets are serialized in a
734+ deterministic order. This wrapping is applied even when a custom ``encoder`` is given.
697735 :return: A AASToJsonEncoder (sub)class.
698736 """
699- if encoder is not None :
700- return encoder
701- return AASToJsonEncoder if not stripped else StrippedAASToJsonEncoder
737+ encoder_ = encoder if encoder is not None \
738+ else (StrippedAASToJsonEncoder if stripped else AASToJsonEncoder )
739+ if sort_arrays and not encoder_ .sort_arrays :
740+ return type ("Sorting" + encoder_ .__name__ , (encoder_ ,), {"sort_arrays" : True })
741+ return encoder_
702742
703743
704744def _create_dict (data : model .AbstractObjectStore ,
705- keys_to_types : Iterable [Tuple [str , Type ]] = JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES ) \
745+ keys_to_types : Iterable [Tuple [str , Type ]] = JSON_AAS_TOP_LEVEL_KEYS_TO_TYPES ,
746+ sort : bool = False ) \
706747 -> Dict [str , List [model .Identifiable ]]:
707748 """
708749 Categorizes objects from an AbstractObjectStore into a dictionary based on their types.
@@ -715,6 +756,10 @@ def _create_dict(data: model.AbstractObjectStore,
715756 :param keys_to_types: An iterable of tuples where each tuple contains:
716757 - A string key representing the category name.
717758 - A type to match objects against.
759+ :param sort: If True, each output list is sorted by ``str(obj.id)``, so that the top-level arrays
760+ ("assetAdministrationShells", "submodels", "conceptDescriptions") have deterministic order
761+ across runs (an AbstractObjectStore is backed by an unordered Python set). Defaults to False
762+ to preserve backward compatibility.
718763 :return: A dictionary where keys are category names and values are lists of objects of the corresponding types.
719764 """
720765 objects : Dict [str , List [model .Identifiable ]] = {}
@@ -728,11 +773,14 @@ def _create_dict(data: model.AbstractObjectStore,
728773 objects .setdefault (name , [])
729774 objects [name ].append (obj )
730775 break # Exit the inner loop once a match is found
776+ if sort :
777+ for object_list in objects .values ():
778+ object_list .sort (key = lambda o : str (o .id ))
731779 return objects
732780
733781
734782def object_store_to_json (data : model .AbstractObjectStore , stripped : bool = False ,
735- encoder : Optional [Type [AASToJsonEncoder ]] = None , ** kwargs ) -> str :
783+ encoder : Optional [Type [AASToJsonEncoder ]] = None , sort_arrays : bool = False , ** kwargs ) -> str :
736784 """
737785 Create a json serialization of a set of AAS objects according to 'Details of the Asset Administration Shell',
738786 chapter 5.5
@@ -743,11 +791,15 @@ def object_store_to_json(data: model.AbstractObjectStore, stripped: bool = False
743791 See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91
744792 This parameter is ignored if an encoder class is specified.
745793 :param encoder: The encoder class used to encode the JSON objects
794+ :param sort_arrays: If True, JSON arrays that originate from unordered Python sets (the top-level object lists as
795+ well as set-valued attributes like ``submodel`` and ``isCaseOf``) are sorted by a stable key,
796+ so that the serialized output is deterministic across runs. Defaults to False to preserve
797+ backward compatibility. Independent of the ``sort_keys`` argument passed to :func:`json.dumps`.
746798 :param kwargs: Additional keyword arguments to be passed to :func:`json.dumps`
747799 """
748- encoder_ = _select_encoder (stripped , encoder )
800+ encoder_ = _select_encoder (stripped , encoder , sort_arrays = sort_arrays )
749801 # serialize object to json
750- return json .dumps (_create_dict (data ), cls = encoder_ , ** kwargs )
802+ return json .dumps (_create_dict (data , sort = sort_arrays ), cls = encoder_ , ** kwargs )
751803
752804
753805class _DetachingTextIOWrapper (io .TextIOWrapper ):
@@ -759,7 +811,7 @@ def __exit__(self, exc_type, exc_val, exc_tb):
759811
760812
761813def write_aas_json_file (file : _generic .PathOrIO , data : model .AbstractObjectStore , stripped : bool = False ,
762- encoder : Optional [Type [AASToJsonEncoder ]] = None , ** kwargs ) -> None :
814+ encoder : Optional [Type [AASToJsonEncoder ]] = None , sort_arrays : bool = False , ** kwargs ) -> None :
763815 """
764816 Write a set of AAS objects to an Asset Administration Shell JSON file according to 'Details of the Asset
765817 Administration Shell', chapter 5.5
@@ -771,9 +823,13 @@ def write_aas_json_file(file: _generic.PathOrIO, data: model.AbstractObjectStore
771823 See https://git.rwth-aachen.de/acplt/pyi40aas/-/issues/91
772824 This parameter is ignored if an encoder class is specified.
773825 :param encoder: The encoder class used to encode the JSON objects
826+ :param sort_arrays: If True, JSON arrays that originate from unordered Python sets (the top-level object lists as
827+ well as set-valued attributes like ``submodel`` and ``isCaseOf``) are sorted by a stable key,
828+ so that the serialized output is deterministic across runs. Defaults to False to preserve
829+ backward compatibility. Independent of the ``sort_keys`` argument passed to :func:`json.dump`.
774830 :param kwargs: Additional keyword arguments to be passed to `json.dump()`
775831 """
776- encoder_ = _select_encoder (stripped , encoder )
832+ encoder_ = _select_encoder (stripped , encoder , sort_arrays = sort_arrays )
777833
778834 # json.dump() only accepts TextIO
779835 cm : ContextManager [TextIO ]
@@ -791,4 +847,4 @@ def write_aas_json_file(file: _generic.PathOrIO, data: model.AbstractObjectStore
791847
792848 # serialize object to json
793849 with cm as fp :
794- json .dump (_create_dict (data ), fp , cls = encoder_ , ** kwargs )
850+ json .dump (_create_dict (data , sort = sort_arrays ), fp , cls = encoder_ , ** kwargs )
0 commit comments