diff --git a/ucamlookup/forms.py b/ucamlookup/forms.py
new file mode 100644
index 0000000000000000000000000000000000000000..39ca9e03b10489f1e14d33330f9d7f7efd62a414
--- /dev/null
+++ b/ucamlookup/forms.py
@@ -0,0 +1,59 @@
+import re
+
+from django import forms
+from django.core.exceptions import ValidationError
+from django.http import QueryDict
+
+from ucamlookup import get_or_create_user_by_crsid
+
+
+class UserListField(forms.Field):
+    """
+    A custom `forms.Field` that validates either is single crsid or a list of crsids and replaces
+    them with a list of `User` objects. If a `User` doesn't exist, it is created.
+    """
+    def clean(self, crsids):
+        users = ()
+
+        if crsids is None:
+            return users
+
+        if isinstance(crsids, str):
+            crsids = [crsids]
+
+        crsid_re = re.compile(r'^[a-z][a-z0-9]{3,7}$')
+        for crsid in crsids:
+            if crsid_re.match(crsid):
+                users += (get_or_create_user_by_crsid(crsid),)
+            else:
+                raise ValidationError("The list of users contains an invalid user")
+
+        return users
+
+
+def convert_query_dict(query_dict):
+    """
+    This function takes a QueryDict converts it to a normal dict, where single parameters are
+    converted to strings and parameter arrays remain as lists. For example a QueryDict representing
+    `?a=1&a=2&b=3` will be converted to `{a: ['1', '2'], b: '3'}`.
+
+    This works around a problem with django forms where the `clean()` method of a `form.Field`
+    expects to receive a list of data but only receives the first element of that list
+    (See: https://code.djangoproject.com/ticket/25347).
+
+    It is used is conjunction with `UserListField` as follows:
+
+    ```python
+    class MyForm(forms.Form):
+        a_field = UserListField()
+
+    def a_handler(request):
+        form = MyForm(convert_query_dict(request.POST))
+    ```
+    """
+    assert isinstance(query_dict, QueryDict)
+
+    return {
+        item[0]: item[1][0] if len(item[1]) == 1 else item[1]
+        for item in dict(query_dict).items()
+    }