diff --git a/keg_elements/forms/validators.py b/keg_elements/forms/validators.py
index f65db1d..d473d7a 100644
--- a/keg_elements/forms/validators.py
+++ b/keg_elements/forms/validators.py
@@ -47,8 +47,18 @@ class ValidateUnique(object):
     def __init__(self, object_html_link=None):
         self.object_html_link = object_html_link
 
+    def get_obj(self, form):
+        if hasattr(form, 'obj'):
+            return form.obj
+        if hasattr(form, '_obj'):
+            return form._obj
+
+        raise AttributeError(
+            'Form must provide either `obj` or `_obj` property for uniqueness validation.'
+        )
+
     def __call__(self, form, field):
-        obj = getattr(form, 'obj')
+        obj = self.get_obj(form)
         other = form.get_object_by_field(field)
 
         both_exist = None not in (obj, other)
@@ -57,7 +67,7 @@ class ValidateUnique(object):
 
         if (both_exist and not same_record) or another_exists_with_value:
             link = (' to {}.'.format(self.object_html_link(other))
-                    if hasattr(self, 'object_html_link') else '.')
+                    if self.object_html_link is not None else '.')
             msg = jinja2.Markup('This value must be unique but is already assigned'
                                 '{}'.format(link))
             raise ValidationError(msg)

`ValidateUnique` form validator can integrate better
Right now `ValidateUnique` requires that the Form have a `obj` member. This must be added manually to the form since it's not included by default on a WTForm form. However, WTForms do have a `_obj` member which has the same purpose. We could make `ValidateUnique` easier to use by utilizing this member.
diff --git a/keg_elements/tests/test_forms/test_validators.py b/keg_elements/tests/test_forms/test_validators.py
index 103894d..e390e5d 100644
--- a/keg_elements/tests/test_forms/test_validators.py
+++ b/keg_elements/tests/test_forms/test_validators.py
@@ -1,24 +1,67 @@
+import jinja2
 import pytest
 import wtforms
+from werkzeug.datastructures import MultiDict
+
 import keg_elements.forms.validators as validators
+from keg_elements.forms import ModelForm
 
 
-class Form():
-    def __init__(self, obj):
-        self.obj = obj
+class UniqueForm(ModelForm):
+    uq_field = wtforms.fields.StringField('thing', validators=[
+        validators.ValidateUnique(object_html_link=lambda field: 'link')])
 
     def get_object_by_field(self, field):
-        return 1
+        return 1 if field.data == '1' else None
 
 
 class TestUniqueValidator(object):
-    def test_unique_validator(self):
-        field = wtforms.fields.StringField('thing')
-        validator = validators.ValidateUnique(object_html_link=lambda field: 'link')
+    def test_validation_passes(self):
+        form = UniqueForm(MultiDict({'uq_field': '1'}), obj=1)
+        assert form.uq_field.validate(form) is True
+
+        form = UniqueForm(MultiDict({'uq_field': '2'}), obj=1)
+        assert form.uq_field.validate(form) is True
+
+    def test_validation_fails(self):
+        form = UniqueForm(MultiDict({'uq_field': '1'}))
+        assert form.uq_field.validate(form) is False
+        assert form.uq_field.errors == [
+            jinja2.Markup('This value must be unique but is already assigned to link.')
+        ]
+
+        form = UniqueForm(MultiDict({'uq_field': '1'}), obj=2)
+        assert form.uq_field.validate(form) is False
+        assert form.uq_field.errors == [
+            jinja2.Markup('This value must be unique but is already assigned to link.')
+        ]
+
+    def test_no_object_link_provided(self):
+        class Form(ModelForm):
+            uq_field = wtforms.fields.StringField('thing', validators=[validators.ValidateUnique()])
+
+            def get_object_by_field(self, field):
+                return 1 if field.data == '1' else None
+
+        form = Form(MultiDict({'uq_field': '1'}))
+        assert form.uq_field.validate(form) is False
+        assert form.uq_field.errors == [
+            jinja2.Markup('This value must be unique but is already assigned.')
+        ]
+
+    def test_get_obj(self):
+        class Form:
+            def __init__(self, **kwargs):
+                self.__dict__.update(kwargs)
+
+        validator = validators.ValidateUnique()
+
+        assert validator.get_obj(Form(obj='foo')) == 'foo'
+        assert validator.get_obj(Form(_obj='foo')) == 'foo'
 
-        assert validator(Form(1), field) is True
+        with pytest.raises(AttributeError) as exc:
+            validator.get_obj(Form())
 
-        with pytest.raises(wtforms.ValidationError,
-                           message=('This valuue must be unique bit is already '
-                                    'assigned link.')):
-            assert validator(Form(2), field)
+        assert str(exc.value) == (
+            'Form must provide either `obj` or `_obj` property for uniqueness validation.'
+        )

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest
3.9
requirements/runtime.txt requirements/testing.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
appdirs==1.4.4
arrow==1.3.0
beautifulsoup4==4.13.3
BlazeUtils==0.7.0
blinker==1.9.0
cffi==1.17.1
click==8.1.8
coverage==7.8.0
cryptography==44.0.2
cssselect==1.3.0
exceptiongroup==1.2.2
flake8==7.2.0
Flask==3.1.0
Flask-SQLAlchemy==3.1.1
Flask-WebTest==0.1.6
Flask-WTF==1.2.2
greenlet==3.1.1
importlib_metadata==8.6.1
infinity==1.5
iniconfig==2.1.0
intervals==0.9.2
itsdangerous==2.2.0
Jinja2==3.1.6
Keg==0.11.1
-e git+https://github.com/level12/keg-elements.git@7838f08ef0e8fec7e2e74c8dcad21f748e98a697#egg=KegElements
lxml==5.3.1
MarkupSafe==3.0.2
mccabe==0.7.0
packaging==24.2
pluggy==1.5.0
pycodestyle==2.13.0
pycparser==2.22
pyflakes==3.3.2
pyquery==2.0.1
pytest==8.3.5
pytest-cov==6.0.0
python-dateutil==2.9.0.post0
python-json-logger==3.3.0
pytz==2025.2
six==1.17.0
soupsieve==2.6
SQLAlchemy==2.0.40
SQLAlchemy-Utils==0.41.2
tomli==2.2.1
types-python-dateutil==2.9.0.20241206
typing_extensions==4.13.0
validators==0.34.0
waitress==3.0.2
WebOb==1.8.9
WebTest==3.0.4
Werkzeug==3.1.3
wrapt==1.17.2
WTForms==3.2.1
WTForms-Alchemy==0.19.0
WTForms-Components==0.11.0
zipp==3.21.0

name: keg-elements
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - appdirs==1.4.4
      - arrow==1.3.0
      - beautifulsoup4==4.13.3
      - blazeutils==0.7.0
      - blinker==1.9.0
      - cffi==1.17.1
      - click==8.1.8
      - coverage==7.8.0
      - cryptography==44.0.2
      - cssselect==1.3.0
      - exceptiongroup==1.2.2
      - flake8==7.2.0
      - flask==3.1.0
      - flask-sqlalchemy==3.1.1
      - flask-webtest==0.1.6
      - flask-wtf==1.2.2
      - greenlet==3.1.1
      - importlib-metadata==8.6.1
      - infinity==1.5
      - iniconfig==2.1.0
      - intervals==0.9.2
      - itsdangerous==2.2.0
      - jinja2==3.1.6
      - keg==0.11.1
      - lxml==5.3.1
      - markupsafe==3.0.2
      - mccabe==0.7.0
      - packaging==24.2
      - pluggy==1.5.0
      - pycodestyle==2.13.0
      - pycparser==2.22
      - pyflakes==3.3.2
      - pyquery==2.0.1
      - pytest==8.3.5
      - pytest-cov==6.0.0
      - python-dateutil==2.9.0.post0
      - python-json-logger==3.3.0
      - pytz==2025.2
      - six==1.17.0
      - soupsieve==2.6
      - sqlalchemy==2.0.40
      - sqlalchemy-utils==0.41.2
      - tomli==2.2.1
      - types-python-dateutil==2.9.0.20241206
      - typing-extensions==4.13.0
      - validators==0.34.0
      - waitress==3.0.2
      - webob==1.8.9
      - webtest==3.0.4
      - werkzeug==3.1.3
      - wrapt==1.17.2
      - wtforms==3.2.1
      - wtforms-alchemy==0.19.0
      - wtforms-components==0.11.0
      - zipp==3.21.0
prefix: /opt/conda/envs/keg-elements

keg_elements/tests/test_forms/test_validators.py::TestUniqueValidator::test_get_obj
keg_elements/tests/test_forms/test_validators.py::TestUniqueValidator::test_validation_passes keg_elements/tests/test_forms/test_validators.py::TestUniqueValidator::test_validation_fails keg_elements/tests/test_forms/test_validators.py::TestUniqueValidator::test_no_object_link_provided
keg_elements/forms/validators.py
keg_elements/forms/validators.py