uranusjr commented on code in PR #71536:
URL: https://github.com/apache/airflow/pull/71536#discussion_r3793779501


##########
airflow-core/src/airflow/serialization/stub_arg_bindings.py:
##########
@@ -108,36 +109,76 @@ def _infer_value_schema(annotation: Any) -> dict[str, 
Any] | None:
         # get_type_hints normalizes a bare ``None`` annotation to NoneType; a 
parameter
         # that can only ever be None constrains nothing worth shipping.
         return None
+    wire_form = _get_wire_form(annotation)
+    # Deep-copy so callers embedding the fragment never alias the cached dict.
+    return copy.deepcopy(wire_form.schema) if wire_form else None
+
+
+class _ValueWireForm(NamedTuple):
+    """The schema describing an annotation's JSON form, and the adapter that 
renders values into it."""
+
+    adapter: TypeAdapter
+    schema: dict[str, Any]
+
+
+def _get_wire_form(annotation: Any) -> _ValueWireForm | None:
     try:
-        schema = _generate_value_schema(annotation)
+        return _build_wire_form(annotation)
     except TypeError:
-        # Unhashable annotations cannot key the cache; generate directly. Any 
pydantic
+        # Unhashable annotations cannot key the cache; build directly. Any 
pydantic
         # failure inside the body degrades to None there, so this retry never 
re-raises.
-        schema = _generate_value_schema.__wrapped__(annotation)
-    # Deep-copy so callers embedding the fragment never alias the cached dict.
-    return copy.deepcopy(schema) if schema else None
+        return _build_wire_form.__wrapped__(annotation)
 
 
 @cache
-def _generate_value_schema(annotation: Any) -> dict[str, Any] | None:
+def _build_wire_form(annotation: Any) -> _ValueWireForm | None:
     """
-    Generate the schema for one annotation, cached for the process lifetime.
+    Build the adapter and schema for one annotation together, cached for the 
process lifetime.
 
     TypeAdapter construction is one of pydantic's most expensive operations and
     annotations are static, so re-serializations of the same Dag must not 
re-pay it.
+
+    Pairing them is what keeps a literal from being rendered in a spelling its 
own
+    ``value_schema`` does not describe: both come from the same adapter, 
including when
+    the temporal-normalization retry below settles on a different annotation.
     """
     # PydanticUserError/TypeError cover annotations pydantic can't schema; 
either way,
     # that degrades to no schema rather than failing Dag serialization.
-    try:
-        return 
TypeAdapter(annotation).json_schema(schema_generator=_ValueSchemaGenerator)
-    except (PydanticUserError, TypeError):
-        normalized = _normalize_temporal_annotation(annotation)
-        if normalized is annotation:
-            return None
+    for candidate in (annotation, _normalize_temporal_annotation(annotation)):
         try:
-            return 
TypeAdapter(normalized).json_schema(schema_generator=_ValueSchemaGenerator)
+            adapter = TypeAdapter(candidate)
+            return _ValueWireForm(adapter, 
adapter.json_schema(schema_generator=_ValueSchemaGenerator))
         except (PydanticUserError, TypeError):
-            return None
+            continue
+    return None
+
+
+def _to_json_value(value: Any, annotation: Any) -> Any:
+    """
+    Render a native value in the JSON form its ``value_schema`` advertises.
+
+    A ``datetime``/``timedelta``/``UUID`` is not JSON-serializable, so without 
this it
+    could not cross the language boundary at all. Dumping it through the same 
adapter
+    that produced the schema gives every lang SDK exactly one spelling per 
format --
+    RFC 3339 timestamps, ISO-8601 durations, canonical UUIDs -- instead of 
each Dag
+    author picking their own.
+
+    Values pydantic cannot render for this annotation pass through untouched, 
leaving
+    the JSON-serializability check to reject them.
+    """
+    if annotation is Parameter.empty or annotation is None or annotation is 
Any:
+        return value
+    if isinstance(value, datetime.datetime):
+        # A naive timestamp is ambiguous once it leaves Python: Go would read 
it as UTC,
+        # JavaScript as the worker's local time, and Java would refuse to 
parse it. Pin
+        # the offset here, using the same default timezone the rest of Airflow 
applies.
+        value = coerce_datetime(value)

Review Comment:
   This probably also need to go into collection types? Otherwise it would miss 
e.g. `list[datetime]`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to