From 03b3b1144c5d9ae869c754e6e417f00ba920dd6a Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Sun, 22 Mar 2026 01:25:04 +0800 Subject: [PATCH] fix: resolve nested ROS2 message types losing properties in JSON Schema generation Array fields containing nested message types (e.g., Point[]) were serialized as {type: object} without inner properties. Now correctly extracts and includes all nested fields, required list, and title via NamespacedType introspection. Made-with: Cursor --- msgcenterpy/core/type_info.py | 7 +++++ msgcenterpy/instances/ros2_instance.py | 42 ++++++++++++++++++++++++-- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/msgcenterpy/core/type_info.py b/msgcenterpy/core/type_info.py index c97d1b3..13ae566 100644 --- a/msgcenterpy/core/type_info.py +++ b/msgcenterpy/core/type_info.py @@ -277,9 +277,16 @@ class TypeInfo: # Special handling for object types if self.is_object and self.object_fields: properties = {} + required_fields = [] for field_name, field_info in self.object_fields.items(): properties[field_name] = field_info.to_json_schema_property(include_constraints) + if field_info.has_constraint(ConstraintType.REQUIRED): + required_fields.append(field_name) property_schema["properties"] = properties + if required_fields: + property_schema["required"] = required_fields + if self.field_name and self.field_name != Consts.ELEMENT_TYPE_INFO_SYMBOL: + property_schema["title"] = self.field_name # Add description if self.original_type: diff --git a/msgcenterpy/instances/ros2_instance.py b/msgcenterpy/instances/ros2_instance.py index b8bcf7a..064d0e3 100644 --- a/msgcenterpy/instances/ros2_instance.py +++ b/msgcenterpy/instances/ros2_instance.py @@ -1,8 +1,11 @@ import array import importlib +import logging from collections import OrderedDict from typing import TYPE_CHECKING, Any, Dict, Optional, Type +logger = logging.getLogger(__name__) + from rosidl_parser.definition import NamespacedType # type: ignore from rosidl_runtime_py import ( # type: ignore import_message_from_namespaced_type, @@ -217,10 +220,9 @@ class ROS2MessageInstance(MessageInstance[Any]): # 基础类型的约束将在 field_accessor 中自动添加 pass elif isinstance(definition_type, NamespacedType): - # 对象类型,标记为对象并提取字段信息 type_info.is_object = True type_info.add_constraint(ConstraintType.TYPE_KEEP, True) - # 这里可以进一步扩展来提取对象字段信息 + self._extract_namespaced_type_fields(type_info, definition_type) # 提取元素类型信息 if get_element_type: if not isinstance(definition_type, AbstractNestedType): @@ -236,3 +238,39 @@ class ROS2MessageInstance(MessageInstance[Any]): original_type=definition_type.value_type, ) self._extract_from_rosidl_definition(type_info.element_type_info) + + def _extract_namespaced_type_fields(self, type_info: TypeInfo, namespaced_type: "NamespacedType") -> None: + """从 NamespacedType(嵌套 ROS2 消息)中提取所有字段信息,填充 object_fields + + 递归处理嵌套的消息类型,确保多层嵌套的结构也能正确提取。 + + Args: + type_info: 要填充 object_fields 的 TypeInfo 对象 + namespaced_type: rosidl NamespacedType 定义 + """ + from msgcenterpy.core.type_info import TypeInfoPostProcessor + + try: + msg_cls = import_message_from_namespaced_type(namespaced_type) + msg_instance = msg_cls() + + # noinspection PyProtectedMember + slots = msg_instance._fields_and_field_types + slot_types = msg_instance.SLOT_TYPES + + for field_name, slot_type in zip(slots, slot_types): + std_type = TypeConverter.rosidl_definition_to_standard(slot_type) + python_type = TypeConverter.standard_to_python_type(std_type) + field_type_info = TypeInfo( + field_name=field_name, + field_path=f"{type_info.field_path}.{field_name}", + standard_type=std_type, + python_type=python_type, + original_type=slot_type, + ) + self._extract_from_rosidl_definition(field_type_info) + TypeInfoPostProcessor.post_process_type_info(field_type_info) + field_type_info.add_constraint(ConstraintType.REQUIRED, True) + type_info.object_fields[field_name] = field_type_info + except Exception as e: + logger.warning("Failed to extract fields from NamespacedType %s: %s", namespaced_type, e)