diff --git a/mmif/serialize/mmif.py b/mmif/serialize/mmif.py index 68478c7f..245c96aa 100644 --- a/mmif/serialize/mmif.py +++ b/mmif/serialize/mmif.py @@ -14,7 +14,7 @@ import math import warnings from collections import defaultdict -from datetime import datetime +from datetime import datetime, timezone from typing import Any, List, Union, Optional, Dict, cast, Iterator import jsonschema.validators @@ -433,7 +433,7 @@ def new_view(self) -> View: """ new_view = View() new_view.id = self.new_view_id() - new_view.metadata.timestamp = datetime.now() + new_view.metadata.timestamp = datetime.now(timezone.utc) self.add_view(new_view) return new_view diff --git a/mmif/serialize/model.py b/mmif/serialize/model.py index 1bec7b29..f7c56b3f 100644 --- a/mmif/serialize/model.py +++ b/mmif/serialize/model.py @@ -23,7 +23,7 @@ import json import warnings -from datetime import datetime +from datetime import datetime, timezone from typing import Union, Any, Dict, Optional, TypeVar, Generic, Generator, Iterator, Type, Set, ClassVar, List T = TypeVar('T') @@ -402,7 +402,12 @@ def default(self, obj: 'MmifObject'): if hasattr(obj, '_serialize'): return obj._serialize() elif hasattr(obj, 'isoformat'): # for datetime objects - return obj.isoformat() + iso_str = obj.isoformat() + # Use 'Z' suffix for UTC timestamps instead of '+00:00' + # Only replace if the datetime is explicitly in UTC timezone + if hasattr(obj, 'tzinfo') and obj.tzinfo == timezone.utc: + return iso_str.replace('+00:00', 'Z') + return iso_str elif hasattr(obj, '__str__'): return str(obj) else: diff --git a/tests/test_serialize.py b/tests/test_serialize.py index 9e857a00..11b06d87 100644 --- a/tests/test_serialize.py +++ b/tests/test_serialize.py @@ -608,6 +608,33 @@ def test_get_label(self): a = v.new_annotation(AnnotationTypes.BoundingBox) _ = a._get_label() + def test_timestamp_uses_utc_with_z_suffix(self): + """Test that timestamps are in UTC with 'Z' suffix to avoid ambiguity""" + from datetime import timezone + mmif_str = ''' + { + "metadata": {"mmif": "http://mmif.clams.ai/1.0.0"}, + "documents": [], + "views": [] + }''' + mmif_obj = Mmif(mmif_str) + + # Create a new view using new_view() - should set UTC timestamp + new_view = mmif_obj.new_view() + new_view.metadata.app = "http://test.app" + + # Verify the timestamp is timezone-aware and uses UTC + self.assertIsNotNone(new_view.metadata.timestamp) + self.assertIsNotNone(new_view.metadata.timestamp.tzinfo) + self.assertEqual(new_view.metadata.timestamp.tzinfo, timezone.utc) + + # Verify serialization uses 'Z' suffix instead of '+00:00' + serialized = mmif_obj.serialize() + self.assertIn('"timestamp":', serialized) + # The timestamp should end with Z (not +00:00) + self.assertIn('Z"', serialized) + self.assertNotIn('+00:00', serialized) + def test_get_anchor_point(self): mmif = Mmif(validate=False) v1 = mmif.new_view()