diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -612,6 +612,7 @@
     'PhutilArgumentUsageException' => 'parser/argument/exception/PhutilArgumentUsageException.php',
     'PhutilArgumentWorkflow' => 'parser/argument/workflow/PhutilArgumentWorkflow.php',
     'PhutilArray' => 'utils/PhutilArray.php',
+    'PhutilArrayCheck' => 'utils/PhutilArrayCheck.php',
     'PhutilArrayTestCase' => 'utils/__tests__/PhutilArrayTestCase.php',
     'PhutilArrayWithDefaultValue' => 'utils/PhutilArrayWithDefaultValue.php',
     'PhutilAsanaFuture' => 'future/asana/PhutilAsanaFuture.php',
@@ -1619,6 +1620,7 @@
       'ArrayAccess',
       'Iterator',
     ),
+    'PhutilArrayCheck' => 'Phobject',
     'PhutilArrayTestCase' => 'PhutilTestCase',
     'PhutilArrayWithDefaultValue' => 'PhutilArray',
     'PhutilAsanaFuture' => 'FutureProxy',
diff --git a/src/utils/PhutilArrayCheck.php b/src/utils/PhutilArrayCheck.php
new file mode 100644
--- /dev/null
+++ b/src/utils/PhutilArrayCheck.php
@@ -0,0 +1,251 @@
+<?php
+
+final class PhutilArrayCheck
+  extends Phobject {
+
+  private $instancesOf;
+  private $uniqueMethod;
+  private $context;
+
+  private $object;
+  private $method;
+
+  public function setInstancesOf($instances_of) {
+    $this->instancesOf = $instances_of;
+    return $this;
+  }
+
+  public function getInstancesOf() {
+    return $this->instancesOf;
+  }
+
+  public function setUniqueMethod($unique_method) {
+    $this->uniqueMethod = $unique_method;
+    return $this;
+  }
+
+  public function getUniqueMethod() {
+    return $this->uniqueMethod;
+  }
+
+  public function setContext($object, $method) {
+    if (!is_object($object) && !is_string($object)) {
+      throw new Exception(
+        pht(
+          'Expected an object or string for "object" context, got "%s".',
+          phutil_describe_type($object)));
+    }
+
+    if (!is_string($method)) {
+      throw new Exception(
+        pht(
+          'Expected a string for "method" context, got "%s".',
+          phutil_describe_type($method)));
+    }
+
+    $argv = func_get_args();
+    $argv = array_slice($argv, 2);
+
+    $this->context = array(
+      'object' => $object,
+      'method' => $method,
+      'argv' => $argv,
+    );
+
+    return $this;
+  }
+
+  public function checkValues($maps) {
+    foreach ($maps as $idx => $map) {
+      $maps[$idx] = $this->checkValue($map);
+    }
+
+    $unique = $this->getUniqueMethod();
+    if ($unique === null) {
+      $result = array();
+
+      foreach ($maps as $map) {
+        foreach ($map as $value) {
+          $result[] = $value;
+        }
+      }
+    } else {
+      $items = array();
+      foreach ($maps as $idx => $map) {
+        foreach ($map as $key => $value) {
+          $items[$key][$idx] = $value;
+        }
+      }
+
+      foreach ($items as $key => $values) {
+        if (count($values) === 1) {
+          continue;
+        }
+        $this->raiseValueException(
+          pht(
+            'Unexpected return value from calls to "%s(...)". More than one '.
+            'object returned a value with unique key "%s". This key was '.
+            'returned by objects with indexes: %s.',
+            $unique,
+            $key,
+            implode(', ', array_keys($values))));
+      }
+
+      $result = array();
+      foreach ($items as $key => $values) {
+        $result[$key] = head($values);
+      }
+    }
+
+    return $result;
+  }
+
+  public function checkValue($items) {
+    if (!$this->context) {
+      throw new PhutilInvalidStateException('setContext');
+    }
+
+    if (!is_array($items)) {
+      $this->raiseValueException(
+        pht(
+          'Expected value to be a list, got "%s".',
+          phutil_describe_type($items)));
+    }
+
+    $instances_of = $this->getInstancesOf();
+    if ($instances_of !== null) {
+      foreach ($items as $idx => $item) {
+        if (!($item instanceof $instances_of)) {
+          $this->raiseValueException(
+            pht(
+              'Expected value to be a list of objects which are instances of '.
+              '"%s", but item with index "%s" is "%s".',
+              $instances_of,
+              $idx,
+              phutil_describe_type($item)));
+        }
+      }
+    }
+
+    $unique = $this->getUniqueMethod();
+    if ($unique !== null) {
+      if ($instances_of === null) {
+        foreach ($items as $idx => $item) {
+          if (!is_object($item)) {
+            $this->raiseValueException(
+              pht(
+                'Expected value to be a list of objects to support calling '.
+                '"%s" to generate unique keys, but item with index "%s" is '.
+                '"%s".',
+                $unique,
+                $idx,
+                phutil_describe_type($item)));
+          }
+        }
+      }
+
+      $map = array();
+
+      foreach ($items as $idx => $item) {
+        $key = call_user_func(array($item, $unique));
+
+        if (!is_string($key) && !is_int($key)) {
+          $this->raiseValueException(
+            pht(
+              'Expected method "%s->%s()" to return a string or integer for '.
+              'use as a unique key, got "%s" from object at index "%s".',
+              get_class($item),
+              $unique,
+              phutil_describe_type($key),
+              $idx));
+        }
+
+        $key = phutil_string_cast($key);
+
+        $map[$key][$idx] = $item;
+      }
+
+      $result = array();
+      foreach ($map as $key => $values) {
+        if (count($values) === 1) {
+          $result[$key] = head($values);
+          continue;
+        }
+
+        $classes = array();
+        foreach ($values as $value) {
+          $classes[] = get_class($value);
+        }
+        $classes = array_fuse($classes);
+
+        if (count($classes) === 1) {
+          $class_display = head($classes);
+        } else {
+          $class_display = sprintf(
+            '[%s]',
+            implode(', ', $classes));
+        }
+
+        $index_display = array();
+        foreach ($values as $idx => $value) {
+          $index_display[] = pht(
+            '"%s" (%s)',
+            $idx,
+            get_class($value));
+        }
+        $index_display = implode(', ', $index_display);
+
+        $this->raiseValueException(
+          pht(
+            'Expected method "%s->%s()" to return a unique key, got "%s" '.
+            'from %s object(s) at indexes: %s.',
+            $class_display,
+            $unique,
+            $key,
+            phutil_count($values),
+            $index_display));
+      }
+
+      $items = $result;
+    }
+
+    return $items;
+  }
+
+  private function raiseValueException($message) {
+    $context = $this->context;
+
+    $object = $context['object'];
+    $method = $context['method'];
+    $argv = $context['argv'];
+
+    if (is_object($object)) {
+      $object_display = sprintf(
+        '%s->%s',
+        get_class($object),
+        $method);
+    } else {
+      $object_display = sprintf(
+        '%s::%s',
+        $object,
+        $method);
+    }
+
+    if (count($argv)) {
+      $call_display = sprintf(
+        '%s(...)',
+        $object_display);
+    } else {
+      $call_display = sprintf(
+        '%s()',
+        $object_display);
+    }
+
+    throw new Exception(
+      pht(
+        'Unexpected return value from call to "%s": %s.',
+        $call_display,
+        $message));
+  }
+
+}