Repository: AllenNeuralDynamics/aind-data-schema

Issue:
Regressions introduced in latest dev (a7a1b30)
 When running integration tests against the latest version of the dev branch, I got the following issues:
 
1. the `GenericModel` I get import errors since `GenericModel` is no longer available for import from the `aind_data_schema.core.acquisition` module. I can change this on my side (probably should import for an original module), but I will also make a small PR to try to clean this up for everyone and ensure full backwards compatibility

2. A new validation error regarding brightness units:
```pydantic_core._pydantic_core.ValidationError: 1 validation error for Monitor
  Value error, Unit brightness_unit is required when brightness is set. [type=value_error, input_value={'name': 'screen_left', '...angles_unit='radians')]}, input_type=dict]
    For further information visit https://errors.pydantic.dev/2.11/v/value_error
```

I will fix this one on my side since I suspect you want to enforce this, but you may want to consider having a sane default to ensure backwards compatibility.

Discussion:

Fix (patch):
diff --git a/src/aind_data_schema/components/devices.py b/src/aind_data_schema/components/devices.py
index 2bd1908ae..41df9e192 100644
--- a/src/aind_data_schema/components/devices.py
+++ b/src/aind_data_schema/components/devices.py
@@ -3,6 +3,7 @@
 from datetime import date
 from decimal import Decimal
 from enum import Enum
+import logging
 from typing import List, Literal, Optional
 import warnings
 
@@ -618,6 +619,24 @@ class Monitor(Device, DevicePosition):
     )
     brightness_unit: Optional[UnitlessUnit] = Field(default=None, title="Brightness unit")
 
+    @model_validator(mode="before")
+    def add_units_if_needed(cls, data: dict) -> dict:
+        """Add units for contrast and brightness if values are provided but units are missing
+
+        This validator is necessary for backwards compatibility
+        TODO: Remove this validator in v3.0.0
+        """
+
+        if "contrast" in data and data["contrast"] is not None and "contrast_unit" not in data:
+            logging.warning("Adding default unit 'percent' for Monitor.contrast_unit")
+            data["contrast_unit"] = UnitlessUnit.PERCENT
+
+        if "brightness" in data and data["brightness"] is not None and "brightness_unit" not in data:
+            logging.warning("Adding default unit 'percent' for Monitor.brightness_unit")
+            data["brightness_unit"] = UnitlessUnit.PERCENT
+
+        return data
+
 
 class LickSpout(Device):
     """Description of a lick spout"""


Test changes:
diff --git a/tests/test_device.py b/tests/test_device.py
index 12d2ca588..5818323b4 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -21,8 +21,10 @@
     HarpDevice,
     ImagingDeviceType,
     ImmersionMedium,
+    Monitor,
     Objective,
 )
+from aind_data_schema_models.units import UnitlessUnit
 
 
 class DeviceTests(unittest.TestCase):
@@ -208,5 +210,86 @@ def test_deprecated_channel_index(self):
             self.assertIn("Use DAQChannel.port instead", str(w[0].message))
 
 
+class MonitorTests(unittest.TestCase):
+    """tests Monitor schemas"""
+
+    def test_add_units_if_needed_validator(self):
+        """tests the Monitor validator for adding units if needed"""
+
+        monitor_with_contrast_no_unit = Monitor(
+            name="test_monitor",
+            manufacturer=Organization.ASUS,
+            refresh_rate=60,
+            width=1920,
+            height=1080,
+            viewing_distance=15.0,
+            relative_position=[AnatomicalRelative.SUPERIOR],
+            contrast=50,
+        )
+        self.assertEqual(monitor_with_contrast_no_unit.contrast, 50)
+        self.assertEqual(monitor_with_contrast_no_unit.contrast_unit, UnitlessUnit.PERCENT)
+
+        monitor_with_brightness_no_unit = Monitor(
+            name="test_monitor",
+            manufacturer=Organization.ASUS,
+            refresh_rate=60,
+            width=1920,
+            height=1080,
+            viewing_distance=15.0,
+            relative_position=[AnatomicalRelative.SUPERIOR],
+            brightness=75,
+        )
+        self.assertEqual(monitor_with_brightness_no_unit.brightness, 75)
+        self.assertEqual(monitor_with_brightness_no_unit.brightness_unit, UnitlessUnit.PERCENT)
+
+        monitor_with_both_no_units = Monitor(
+            name="test_monitor",
+            manufacturer=Organization.ASUS,
+            refresh_rate=60,
+            width=1920,
+            height=1080,
+            viewing_distance=15.0,
+            relative_position=[AnatomicalRelative.SUPERIOR],
+            contrast=50,
+            brightness=75,
+        )
+        self.assertEqual(monitor_with_both_no_units.contrast, 50)
+        self.assertEqual(monitor_with_both_no_units.contrast_unit, UnitlessUnit.PERCENT)
+        self.assertEqual(monitor_with_both_no_units.brightness, 75)
+        self.assertEqual(monitor_with_both_no_units.brightness_unit, UnitlessUnit.PERCENT)
+
+        monitor_with_explicit_units = Monitor(
+            name="test_monitor",
+            manufacturer=Organization.ASUS,
+            refresh_rate=60,
+            width=1920,
+            height=1080,
+            viewing_distance=15.0,
+            relative_position=[AnatomicalRelative.SUPERIOR],
+            contrast=50,
+            contrast_unit=UnitlessUnit.PERCENT,
+            brightness=75,
+            brightness_unit=UnitlessUnit.PERCENT,
+        )
+        self.assertEqual(monitor_with_explicit_units.contrast, 50)
+        self.assertEqual(monitor_with_explicit_units.contrast_unit, UnitlessUnit.PERCENT)
+        self.assertEqual(monitor_with_explicit_units.brightness, 75)
+        self.assertEqual(monitor_with_explicit_units.brightness_unit, UnitlessUnit.PERCENT)
+
+        monitor_without_contrast_brightness = Monitor(
+            name="test_monitor",
+            manufacturer=Organization.ASUS,
+            refresh_rate=60,
+            width=1920,
+            height=1080,
+            viewing_distance=15.0,
+            relative_position=[AnatomicalRelative.SUPERIOR],
+        )
+        self.assertIsNone(monitor_without_contrast_brightness.contrast)
+        self.assertIsNone(monitor_without_contrast_brightness.contrast_unit)
+        self.assertIsNone(monitor_without_contrast_brightness.brightness)
+        self.assertIsNone(monitor_without_contrast_brightness.brightness_unit)
+
+
 if __name__ == "__main__":
     unittest.main()