codecov-io: # [Codecov](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=h1) Report
> Merging [#69](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=desc) into [master](https://codecov.io/gh/rossmacarthur/serde/commit/ef2580e4983e4e326080c59606527a2038c4c6c4?src=pr&el=desc) will **not change** coverage.
> The diff coverage is `100%`.

[![Impacted file tree graph](https://codecov.io/gh/rossmacarthur/serde/pull/69/graphs/tree.svg?width=650&token=kZWHiIewTt&height=150&src=pr)](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=tree)

```diff
@@          Coverage Diff          @@
##           master    #69   +/-   ##
=====================================
  Coverage     100%   100%           
=====================================
  Files           6      6           
  Lines         537    559   +22     
=====================================
+ Hits          537    559   +22
```


| [Impacted Files](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/serde/validate.py](https://codecov.io/gh/rossmacarthur/serde/pull/69/diff?src=pr&el=tree#diff-c3JjL3NlcmRlL3ZhbGlkYXRlLnB5) | `100% <100%> (ø)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=footer). Last update [ef2580e...5427f89](https://codecov.io/gh/rossmacarthur/serde/pull/69?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

diff --git a/RELEASES.rst b/RELEASES.rst
index 53a17a1..dfb0492 100644
--- a/RELEASES.rst
+++ b/RELEASES.rst
@@ -6,10 +6,14 @@ Releases
 
 *Unreleased*
 
-- Add basestring and unicode to built-in Field map (`#68`_)
+- Add equal and length validators. (`#67`_, `#69`_)
+- Add basestring and unicode to built-in Field map. (`#68`_)
 
+.. _#69: https://github.com/rossmacarthur/serde/pull/69
 .. _#68: https://github.com/rossmacarthur/serde/pull/68
 
+.. _#67: https://github.com/rossmacarthur/serde/issues/67
+
 0.4.1
 -----
 
diff --git a/src/serde/validate.py b/src/serde/validate.py
index 0a07c29..111f1e7 100644
--- a/src/serde/validate.py
+++ b/src/serde/validate.py
@@ -54,6 +54,36 @@ def instance(type):
     return instance_
 
 
+def equal(to):
+    """
+    Validate that a value is equal to something.
+
+    Args:
+        to: the value to check against.
+
+    Returns:
+        function: the validator function.
+    """
+    def equal_(value):
+        """
+        Validate that the given value is equal to the specified value.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value is not equal to
+                the expected value.
+        """
+        if value != to:
+            raise ValidationError(
+                'expected {!r} but got {!r}'
+                .format(to, value)
+            )
+
+    return equal_
+
+
 def min(endpoint, inclusive=True):
     """
     Validate that a value is greater than and/or equal to the given endpoint.
@@ -159,6 +189,115 @@ def between(min_endpoint, max_endpoint, inclusive=True):
     return between_
 
 
+def length(length):
+    """
+    Validate that the given value has a particular length.
+
+    Args:
+        length (int): the length allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_(value):
+        """
+        Validate that the given value has the expected length.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value does not have
+                the expected length.
+        """
+        equal(length)(len(value))
+
+    return length_
+
+
+def length_min(endpoint, inclusive=True):
+    """
+    Validate that a value's length is greater than/or equal to a minimum.
+
+    Args:
+        endpoint: the minimum length allowed.
+        inclusive (bool): whether the minimum length value itself allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_min_(value):
+        """
+        Validate that a value's length is greater than/or equal to a minimum.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value's length is
+                greater than and/or equal to the maximum.
+        """
+        min(endpoint, inclusive=inclusive)(len(value))
+
+    return length_min_
+
+
+def length_max(endpoint, inclusive=True):
+    """
+    Validate that a value's length is less than/or equal to a maximum.
+
+    Args:
+        endpoint: the maximum length allowed.
+        inclusive (bool): whether the maximum length value itself allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_max_(value):
+        """
+        Validate that a value's length is less than/or equal to a maximum.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value's length is
+                less than and/or equal to the maximum.
+        """
+        max(endpoint, inclusive=inclusive)(len(value))
+
+    return length_max_
+
+
+def length_between(min_endpoint, max_endpoint, inclusive=True):
+    """
+    Validate that the given value's length is between a minimum and maximum.
+
+    Args:
+        min_endpoint: the minimum length allowed.
+        max_endpoint: the maximum length allowed.
+        inclusive (bool): whether the endpoint length values are allowed.
+
+    Returns:
+        function: the validator function.
+    """
+    def length_between_(value):
+        """
+        Validate that the given value's length is between a minimum and maximum.
+
+        Args:
+            value: the value to validate.
+
+        Raises:
+            `~serde.exceptions.ValidationError`: when the value's length is not
+                between the minimum and maximum.
+        """
+        min(min_endpoint, inclusive=inclusive)(len(value))
+        max(max_endpoint, inclusive=inclusive)(len(value))
+
+    return length_between_
+
+
 def contains(allowed):
     """
     Validate that the given list/range/tuple contains the given value.

Add length validate methods
Example

```python
from serde import Model, fields, validate

class Example(Model):
    a = fields.Field(validators=[validate.length(10)])
    b = fields.Field(validators=[validate.length_between(10, 20, inclusive=False)])
    c = fields.Field(validators=[validate.length_min(10, inclusive=True)])
    d = fields.Field(validators=[validate.length_max(10)])
```
diff --git a/tests/test_validate.py b/tests/test_validate.py
index b7d7040..ab9e308 100644
--- a/tests/test_validate.py
+++ b/tests/test_validate.py
@@ -20,6 +20,14 @@ def test_instance():
         validate.instance(float)(1)
 
 
+def test_equal():
+    value = object()
+    validate.equal(value)(value)
+
+    with raises(ValidationError):
+        validate.equal(object())(20)
+
+
 def test_min():
     validate.min(100)(100)
     validate.min(100)(1000)
@@ -53,6 +61,45 @@ def test_between():
         validate.between(-100, 100, inclusive=False)(100)
 
 
+def test_length():
+    validate.length(10)(range(10))
+    validate.length(3)([1, 2, 3])
+
+    with raises(ValidationError):
+        validate.length(10)((1, 2))
+
+
+def test_length_min():
+    validate.length_min(5)(range(10))
+    validate.length_min(5, inclusive=False)(range(6))
+
+    with raises(ValidationError):
+        validate.length_min(5)(range(2))
+
+    with raises(ValidationError):
+        validate.length_min(5, inclusive=False)(range(5))
+
+
+def test_length_max():
+    validate.length_max(5)(range(2))
+
+    with raises(ValidationError):
+        validate.length_max(5)(range(10))
+
+    with raises(ValidationError):
+        validate.length_max(5, inclusive=False)(range(5))
+
+
+def test_length_between():
+    validate.length_between(0, 10)(range(10))
+
+    with raises(ValidationError):
+        validate.length_between(10, 100)(range(5))
+
+    with raises(ValidationError):
+        validate.length_between(0, 10, inclusive=False)([])
+
+
 def test_contains():
     validate.contains(range(5))(3)
     validate.contains((0, 1, 2, 3, 4))(3)

pip install -e .[ext,cbor,toml,yaml,dev.lint,dev.test]
parse_log_pytest
pytest
pytest pytest-cov pytest
apt-get update apt-get install -y gcc
3.9
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
coverage==7.8.0
decorator==5.2.1
exceptiongroup @ file:///croot/exceptiongroup_1706031385326/work
flake8==3.6.0
flake8-docstrings==1.7.0
flake8-isort==6.1.2
flake8-per-file-ignores==0.8.1
flake8-polyfill==1.0.2
flake8-quotes==3.4.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
isodate==0.6.1
isort==6.0.1
mccabe==0.6.1
mock==5.2.0
packaging @ file:///croot/packaging_1734472117206/work
pathmatch==0.2.2
pep8-naming==0.11.1
pluggy @ file:///croot/pluggy_1733169602837/work
pycodestyle==2.4.0
pydocstyle==6.3.0
pyflakes==2.0.0
pytest @ file:///croot/pytest_1738938843180/work
pytest-cov==6.0.0
pytest-doctest-import==0.1.1
ruamel.yaml==0.15.100
-e git+https://github.com/rossmacarthur/serde.git@ef2580e4983e4e326080c59606527a2038c4c6c4#egg=serde
six==1.17.0
snowballstemmer==2.2.0
toml==0.10.2
tomli @ file:///opt/conda/conda-bld/tomli_1657175507142/work
typing==3.7.4.3
validators==0.12.6

name: serde
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
  - exceptiongroup=1.2.0=py39h06a4308_0
  - iniconfig=1.1.1=pyhd3eb1b0_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
  - packaging=24.2=py39h06a4308_0
  - pip=25.0=py39h06a4308_0
  - pluggy=1.5.0=py39h06a4308_0
  - pytest=8.3.4=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
  - tomli=2.0.1=py39h06a4308_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - coverage==7.8.0
      - decorator==5.2.1
      - flake8==3.6.0
      - flake8-docstrings==1.7.0
      - flake8-isort==6.1.2
      - flake8-per-file-ignores==0.8.1
      - flake8-polyfill==1.0.2
      - flake8-quotes==3.4.0
      - isodate==0.6.1
      - isort==6.0.1
      - mccabe==0.6.1
      - mock==5.2.0
      - pathmatch==0.2.2
      - pep8-naming==0.11.1
      - pycodestyle==2.4.0
      - pydocstyle==6.3.0
      - pyflakes==2.0.0
      - pytest-cov==6.0.0
      - pytest-doctest-import==0.1.1
      - ruamel-yaml==0.15.100
      - six==1.17.0
      - snowballstemmer==2.2.0
      - toml==0.10.2
      - typing==3.7.4.3
      - validators==0.12.6
prefix: /opt/conda/envs/serde

tests/test_validate.py::test_equal tests/test_validate.py::test_length tests/test_validate.py::test_length_min tests/test_validate.py::test_length_max tests/test_validate.py::test_length_between
tests/test_validate.py::test_instance tests/test_validate.py::test_min tests/test_validate.py::test_max tests/test_validate.py::test_between tests/test_validate.py::test_contains tests/test_validate.py::test_domain tests/test_validate.py::test_email tests/test_validate.py::test_ip_address tests/test_validate.py::test_ipv4_address tests/test_validate.py::test_ipv6_address tests/test_validate.py::test_mac_address tests/test_validate.py::test_slug tests/test_validate.py::test_url
RELEASES.rst src/serde/validate.py
RELEASES.rst src/serde/validate.py