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
@@ -135,6 +135,7 @@
     'AphrontFormView' => 'view/form/AphrontFormView.php',
     'AphrontGlyphBarView' => 'view/widget/bars/AphrontGlyphBarView.php',
     'AphrontHTMLResponse' => 'aphront/response/AphrontHTMLResponse.php',
+    'AphrontHTTPParameterType' => 'aphront/httpparametertype/AphrontHTTPParameterType.php',
     'AphrontHTTPProxyResponse' => 'aphront/response/AphrontHTTPProxyResponse.php',
     'AphrontHTTPSink' => 'aphront/sink/AphrontHTTPSink.php',
     'AphrontHTTPSinkTestCase' => 'aphront/sink/__tests__/AphrontHTTPSinkTestCase.php',
@@ -149,6 +150,8 @@
     'AphrontMultiColumnView' => 'view/layout/AphrontMultiColumnView.php',
     'AphrontMySQLDatabaseConnectionTestCase' => 'infrastructure/storage/__tests__/AphrontMySQLDatabaseConnectionTestCase.php',
     'AphrontNullView' => 'view/AphrontNullView.php',
+    'AphrontPHIDHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDHTTPParameterType.php',
+    'AphrontPHIDListHTTPParameterType' => 'aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php',
     'AphrontPHPHTTPSink' => 'aphront/sink/AphrontPHPHTTPSink.php',
     'AphrontPageView' => 'view/page/AphrontPageView.php',
     'AphrontPlainTextResponse' => 'aphront/response/AphrontPlainTextResponse.php',
@@ -164,10 +167,13 @@
     'AphrontResponseProducerInterface' => 'aphront/interface/AphrontResponseProducerInterface.php',
     'AphrontRoutingMap' => 'aphront/site/AphrontRoutingMap.php',
     'AphrontRoutingResult' => 'aphront/site/AphrontRoutingResult.php',
+    'AphrontSelectHTTPParameterType' => 'aphront/httpparametertype/AphrontSelectHTTPParameterType.php',
     'AphrontSideNavFilterView' => 'view/layout/AphrontSideNavFilterView.php',
     'AphrontSite' => 'aphront/site/AphrontSite.php',
     'AphrontStackTraceView' => 'view/widget/AphrontStackTraceView.php',
     'AphrontStandaloneHTMLResponse' => 'aphront/response/AphrontStandaloneHTMLResponse.php',
+    'AphrontStringHTTPParameterType' => 'aphront/httpparametertype/AphrontStringHTTPParameterType.php',
+    'AphrontStringListHTTPParameterType' => 'aphront/httpparametertype/AphrontStringListHTTPParameterType.php',
     'AphrontTableView' => 'view/control/AphrontTableView.php',
     'AphrontTagView' => 'view/AphrontTagView.php',
     'AphrontTokenizerTemplateView' => 'view/control/AphrontTokenizerTemplateView.php',
@@ -1889,6 +1895,7 @@
     'PhabricatorConfigEntryQuery' => 'applications/config/query/PhabricatorConfigEntryQuery.php',
     'PhabricatorConfigFileSource' => 'infrastructure/env/PhabricatorConfigFileSource.php',
     'PhabricatorConfigGroupController' => 'applications/config/controller/PhabricatorConfigGroupController.php',
+    'PhabricatorConfigHTTPParameterTypesModule' => 'applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php',
     'PhabricatorConfigHistoryController' => 'applications/config/controller/PhabricatorConfigHistoryController.php',
     'PhabricatorConfigIgnoreController' => 'applications/config/controller/PhabricatorConfigIgnoreController.php',
     'PhabricatorConfigIssueListController' => 'applications/config/controller/PhabricatorConfigIssueListController.php',
@@ -2250,6 +2257,7 @@
     'PhabricatorGlobalLock' => 'infrastructure/util/PhabricatorGlobalLock.php',
     'PhabricatorGlobalUploadTargetView' => 'applications/files/view/PhabricatorGlobalUploadTargetView.php',
     'PhabricatorGoogleAuthProvider' => 'applications/auth/provider/PhabricatorGoogleAuthProvider.php',
+    'PhabricatorHTTPParameterTypeTableView' => 'applications/config/view/PhabricatorHTTPParameterTypeTableView.php',
     'PhabricatorHandleList' => 'applications/phid/handle/pool/PhabricatorHandleList.php',
     'PhabricatorHandleObjectSelectorDataView' => 'applications/phid/handle/view/PhabricatorHandleObjectSelectorDataView.php',
     'PhabricatorHandlePool' => 'applications/phid/handle/pool/PhabricatorHandlePool.php',
@@ -3866,6 +3874,7 @@
     'AphrontFormView' => 'AphrontView',
     'AphrontGlyphBarView' => 'AphrontBarView',
     'AphrontHTMLResponse' => 'AphrontResponse',
+    'AphrontHTTPParameterType' => 'Phobject',
     'AphrontHTTPProxyResponse' => 'AphrontResponse',
     'AphrontHTTPSink' => 'Phobject',
     'AphrontHTTPSinkTestCase' => 'PhabricatorTestCase',
@@ -3880,6 +3889,8 @@
     'AphrontMultiColumnView' => 'AphrontView',
     'AphrontMySQLDatabaseConnectionTestCase' => 'PhabricatorTestCase',
     'AphrontNullView' => 'AphrontView',
+    'AphrontPHIDHTTPParameterType' => 'AphrontHTTPParameterType',
+    'AphrontPHIDListHTTPParameterType' => 'AphrontHTTPParameterType',
     'AphrontPHPHTTPSink' => 'AphrontHTTPSink',
     'AphrontPageView' => 'AphrontView',
     'AphrontPlainTextResponse' => 'AphrontResponse',
@@ -3897,10 +3908,13 @@
     'AphrontResponse' => 'Phobject',
     'AphrontRoutingMap' => 'Phobject',
     'AphrontRoutingResult' => 'Phobject',
+    'AphrontSelectHTTPParameterType' => 'AphrontHTTPParameterType',
     'AphrontSideNavFilterView' => 'AphrontView',
     'AphrontSite' => 'Phobject',
     'AphrontStackTraceView' => 'AphrontView',
     'AphrontStandaloneHTMLResponse' => 'AphrontHTMLResponse',
+    'AphrontStringHTTPParameterType' => 'AphrontHTTPParameterType',
+    'AphrontStringListHTTPParameterType' => 'AphrontHTTPParameterType',
     'AphrontTableView' => 'AphrontView',
     'AphrontTagView' => 'AphrontView',
     'AphrontTokenizerTemplateView' => 'AphrontView',
@@ -5889,6 +5903,7 @@
     'PhabricatorConfigEntryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorConfigFileSource' => 'PhabricatorConfigProxySource',
     'PhabricatorConfigGroupController' => 'PhabricatorConfigController',
+    'PhabricatorConfigHTTPParameterTypesModule' => 'PhabricatorConfigModule',
     'PhabricatorConfigHistoryController' => 'PhabricatorConfigController',
     'PhabricatorConfigIgnoreController' => 'PhabricatorConfigController',
     'PhabricatorConfigIssueListController' => 'PhabricatorConfigController',
@@ -6312,6 +6327,7 @@
     'PhabricatorGlobalLock' => 'PhutilLock',
     'PhabricatorGlobalUploadTargetView' => 'AphrontView',
     'PhabricatorGoogleAuthProvider' => 'PhabricatorOAuth2AuthProvider',
+    'PhabricatorHTTPParameterTypeTableView' => 'AphrontView',
     'PhabricatorHandleList' => array(
       'Phobject',
       'Iterator',
diff --git a/src/aphront/httpparametertype/AphrontHTTPParameterType.php b/src/aphront/httpparametertype/AphrontHTTPParameterType.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/httpparametertype/AphrontHTTPParameterType.php
@@ -0,0 +1,309 @@
+<?php
+
+/**
+ * Defines how to read a complex value from an HTTP request.
+ *
+ * Most HTTP parameters are simple (like strings or integers) but some
+ * parameters accept more complex values (like lists of users or project names).
+ *
+ * This class handles reading simple and complex values from a request,
+ * performing any required parsing or lookups, and returning a result in a
+ * standard format.
+ *
+ * @task read Reading Values from a Request
+ * @task info Information About the Type
+ * @task util Parsing Utilities
+ * @task impl Implementation
+ */
+abstract class AphrontHTTPParameterType extends Phobject {
+
+
+  private $viewer;
+
+
+/* -(  Reading Values from a Request  )-------------------------------------- */
+
+
+  /**
+   * Set the current viewer.
+   *
+   * Some parameter types perform complex parsing involving lookups. For
+   * example, a type might lookup usernames or project names. These types need
+   * to use the current viewer to execute queries.
+   *
+   * @param PhabricatorUser Current viewer.
+   * @return this
+   * @task read
+   */
+  final public function setViewer(PhabricatorUser $viewer) {
+    $this->viewer = $viewer;
+    return $this;
+  }
+
+
+  /**
+   * Get the current viewer.
+   *
+   * @return PhabricatorUser Current viewer.
+   * @task read
+   */
+  final public function getViewer() {
+    if (!$this->viewer) {
+      throw new PhutilInvalidStateException('setViewer');
+    }
+    return $this->viewer;
+  }
+
+
+  /**
+   * Test if a value is present in a request.
+   *
+   * @param AphrontRequest The incoming request.
+   * @param string The key to examine.
+   * @return bool True if a readable value is present in the request.
+   * @task read
+   */
+  final public function getExists(AphrontRequest $request, $key) {
+    return $this->getParameterExists($request, $key);
+  }
+
+
+  /**
+   * Read a value from a request.
+   *
+   * If the value is not present, a default value is returned (usually `null`).
+   * Use @{method:getExists} to test if a value is present.
+   *
+   * @param AphrontRequest The incoming request.
+   * @param string The key to examine.
+   * @return wild Value, or default if value is not present.
+   * @task read
+   */
+  final public function getValue(AphrontRequest $request, $key) {
+
+    if (!$this->getExists($request, $key)) {
+      return $this->getParameterDefault();
+    }
+
+    return $this->getParameterValue($request, $key);
+  }
+
+
+  /**
+   * Get the default value for this parameter type.
+   *
+   * @return wild Default value for this type.
+   * @task read
+   */
+  final public function getDefaultValue() {
+    return $this->getParameterDefault();
+  }
+
+
+/* -(  Information About the Type  )----------------------------------------- */
+
+
+  /**
+   * Get a short name for this type, like `string` or `list<phid>`.
+   *
+   * @return string Short type name.
+   * @task info
+   */
+  final public function getTypeName() {
+    return $this->getParameterTypeName();
+  }
+
+
+  /**
+   * Get a list of human-readable descriptions of acceptable formats for this
+   * type.
+   *
+   * For example, a type might return strings like these:
+   *
+   * > Any positive integer.
+   * > A comma-separated list of PHIDs.
+   *
+   * This is used to explain to users how to specify a type when generating
+   * documentation.
+   *
+   * @return list<string> Human-readable list of acceptable formats.
+   * @task info
+   */
+  final public function getFormatDescriptions() {
+    return $this->getParameterFormatDescriptions();
+  }
+
+
+  /**
+   * Get a list of human-readable examples of how to format this type as an
+   * HTTP GET parameter.
+   *
+   * For example, a type might return strings like these:
+   *
+   * > v=123
+   * > v[]=1&v[]=2
+   *
+   * This is used to show users how to specify parameters of this type in
+   * generated documentation.
+   *
+   * @return list<string> Human-readable list of format examples.
+   * @task info
+   */
+  final public function getExamples() {
+    return $this->getParameterExamples();
+  }
+
+
+/* -(  Utilities  )---------------------------------------------------------- */
+
+
+  /**
+   * Call another type's existence check.
+   *
+   * This method allows a type to reuse the exitence behavior of a different
+   * type. For example, a "list of users" type may have the same basic
+   * existence check that a simpler "list of strings" type has, and can just
+   * call the simpler type to reuse its behavior.
+   *
+   * @param AphrontHTTPParameterType The other type.
+   * @param AphrontRequest Incoming request.
+   * @param string Key to examine.
+   * @return bool True if the parameter exists.
+   * @task util
+   */
+  final protected function getExistsWithType(
+    AphrontHTTPParameterType $type,
+    AphrontRequest $request,
+    $key) {
+
+    $type->setViewer($this->getViewer());
+
+    return $type->getParameterExists($request, $key);
+  }
+
+
+  /**
+   * Call another type's value parser.
+   *
+   * This method allows a type to reuse the parsing behavior of a different
+   * type. For example, a "list of users" type may start by running the same
+   * basic parsing that a simpler "list of strings" type does.
+   *
+   * @param AphrontHTTPParameterType The other type.
+   * @param AphrontRequest Incoming request.
+   * @param string Key to examine.
+   * @return wild Parsed value.
+   * @task util
+   */
+  final protected function getValueWithType(
+    AphrontHTTPParameterType $type,
+    AphrontRequest $request,
+    $key) {
+
+    $type->setViewer($this->getViewer());
+
+    return $type->getValue($request, $key);
+  }
+
+
+  /**
+   * Get a list of all available parameter types.
+   *
+   * @return list<AphrontHTTPParameterType> List of all available types.
+   * @task util
+   */
+  final public static function getAllTypes() {
+    return id(new PhutilClassMapQuery())
+      ->setAncestorClass(__CLASS__)
+      ->setUniqueMethod('getTypeName')
+      ->setSortMethod('getTypeName')
+      ->execute();
+  }
+
+
+/* -(  Implementation  )----------------------------------------------------- */
+
+
+  /**
+   * Test if a parameter exists in a request.
+   *
+   * See @{method:getExists}. By default, this method tests if the key is
+   * present in the request.
+   *
+   * To call another type's behavior in order to perform this check, use
+   * @{method:getExistsWithType}.
+   *
+   * @param AphrontRequest The incoming request.
+   * @param string The key to examine.
+   * @return bool True if a readable value is present in the request.
+   * @task impl
+   */
+  protected function getParameterExists(AphrontRequest $request, $key) {
+    return $request->getExists($key);
+  }
+
+
+  /**
+   * Parse a value from a request.
+   *
+   * See @{method:getValue}. This method will //only// be called if this type
+   * has already asserted that the value exists with
+   * @{method:getParameterExists}.
+   *
+   * To call another type's behavior in order to parse a value, use
+   * @{method:getValueWithType}.
+   *
+   * @param AphrontRequest The incoming request.
+   * @param string The key to examine.
+   * @return wild Parsed value.
+   * @task impl
+   */
+  abstract protected function getParameterValue(AphrontRequest $request, $key);
+
+
+  /**
+   * Return a simple type name string, like "string" or "list<phid>".
+   *
+   * See @{method:getTypeName}.
+   *
+   * @return string Short type name.
+   * @task impl
+   */
+  abstract protected function getParameterTypeName();
+
+
+  /**
+   * Return a human-readable list of format descriptions.
+   *
+   * See @{method:getFormatDescriptions}.
+   *
+   * @return list<string> Human-readable list of acceptable formats.
+   * @task impl
+   */
+  abstract protected function getParameterFormatDescriptions();
+
+
+  /**
+   * Return a human-readable list of examples.
+   *
+   * See @{method:getExamples}.
+   *
+   * @return list<string> Human-readable list of format examples.
+   * @task impl
+   */
+  abstract protected function getParameterExamples();
+
+
+  /**
+   * Return the default value for this parameter type.
+   *
+   * See @{method:getDefaultValue}. If unspecified, the default is `null`.
+   *
+   * @return wild Default value.
+   * @task impl
+   */
+  protected function getParameterDefault() {
+    return null;
+  }
+
+}
diff --git a/src/aphront/httpparametertype/AphrontPHIDHTTPParameterType.php b/src/aphront/httpparametertype/AphrontPHIDHTTPParameterType.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/httpparametertype/AphrontPHIDHTTPParameterType.php
@@ -0,0 +1,26 @@
+<?php
+
+final class AphrontPHIDHTTPParameterType
+  extends AphrontHTTPParameterType {
+
+  protected function getParameterValue(AphrontRequest $request, $key) {
+    return $request->getStr($key);
+  }
+
+  protected function getParameterTypeName() {
+    return 'phid';
+  }
+
+  protected function getParameterFormatDescriptions() {
+    return array(
+      pht('A single object PHID.'),
+    );
+  }
+
+  protected function getParameterExamples() {
+    return array(
+      'v=PHID-XXXX-1111',
+    );
+  }
+
+}
diff --git a/src/aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php b/src/aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/httpparametertype/AphrontPHIDListHTTPParameterType.php
@@ -0,0 +1,30 @@
+<?php
+
+final class AphrontPHIDListHTTPParameterType
+  extends AphrontHTTPParameterType {
+
+  protected function getParameterValue(AphrontRequest $request, $key) {
+    $type = new AphrontStringListHTTPParameterType();
+    return $this->getValueWithType($type, $request, $key);
+  }
+
+  protected function getParameterTypeName() {
+    return 'list<phid>';
+  }
+
+  protected function getParameterFormatDescriptions() {
+    return array(
+      pht('Comma-separated list of PHIDs.'),
+      pht('List of PHIDs, as array.'),
+    );
+  }
+
+  protected function getParameterExamples() {
+    return array(
+      'v=PHID-XXXX-1111',
+      'v=PHID-XXXX-1111,PHID-XXXX-2222',
+      'v[]=PHID-XXXX-1111&v[]=PHID-XXXX-2222',
+    );
+  }
+
+}
diff --git a/src/aphront/httpparametertype/AphrontSelectHTTPParameterType.php b/src/aphront/httpparametertype/AphrontSelectHTTPParameterType.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/httpparametertype/AphrontSelectHTTPParameterType.php
@@ -0,0 +1,26 @@
+<?php
+
+final class AphrontSelectHTTPParameterType
+  extends AphrontHTTPParameterType {
+
+  protected function getParameterValue(AphrontRequest $request, $key) {
+    return $request->getStr($key);
+  }
+
+  protected function getParameterTypeName() {
+    return 'select';
+  }
+
+  protected function getParameterFormatDescriptions() {
+    return array(
+      pht('A single value from the allowed set.'),
+    );
+  }
+
+  protected function getParameterExamples() {
+    return array(
+      'v=value',
+    );
+  }
+
+}
diff --git a/src/aphront/httpparametertype/AphrontStringHTTPParameterType.php b/src/aphront/httpparametertype/AphrontStringHTTPParameterType.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/httpparametertype/AphrontStringHTTPParameterType.php
@@ -0,0 +1,27 @@
+<?php
+
+final class AphrontStringHTTPParameterType
+  extends AphrontHTTPParameterType {
+
+  protected function getParameterValue(AphrontRequest $request, $key) {
+    return $request->getStr($key);
+  }
+
+  protected function getParameterTypeName() {
+    return 'string';
+  }
+
+  protected function getParameterFormatDescriptions() {
+    return array(
+      pht('A URL-encoded string.'),
+    );
+  }
+
+  protected function getParameterExamples() {
+    return array(
+      'v=simple',
+      'v=properly%20escaped%20text',
+    );
+  }
+
+}
diff --git a/src/aphront/httpparametertype/AphrontStringListHTTPParameterType.php b/src/aphront/httpparametertype/AphrontStringListHTTPParameterType.php
new file mode 100644
--- /dev/null
+++ b/src/aphront/httpparametertype/AphrontStringListHTTPParameterType.php
@@ -0,0 +1,38 @@
+<?php
+
+final class AphrontStringListHTTPParameterType
+  extends AphrontHTTPParameterType {
+
+  protected function getParameterValue(AphrontRequest $request, $key) {
+    $list = $request->getArr($key, null);
+
+    if ($list === null) {
+      $list = $request->getStrList($key);
+    }
+
+    return $list;
+  }
+
+  protected function getParameterDefault() {
+    return array();
+  }
+
+  protected function getParameterTypeName() {
+    return 'list<string>';
+  }
+
+  protected function getParameterFormatDescriptions() {
+    return array(
+      pht('Comma-separated list of strings.'),
+      pht('List of strings, as array.'),
+    );
+  }
+
+  protected function getParameterExamples() {
+    return array(
+      'v=cat,dog,pig',
+      'v[]=cat&v[]=dog',
+    );
+  }
+
+}
diff --git a/src/applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php b/src/applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php
new file mode 100644
--- /dev/null
+++ b/src/applications/config/module/PhabricatorConfigHTTPParameterTypesModule.php
@@ -0,0 +1,27 @@
+<?php
+
+final class PhabricatorConfigHTTPParameterTypesModule
+  extends PhabricatorConfigModule {
+
+  public function getModuleKey() {
+    return 'httpparameter';
+  }
+
+  public function getModuleName() {
+    return pht('HTTP Parameter Types');
+  }
+
+  public function renderModuleStatus(AphrontRequest $request) {
+    $viewer = $request->getViewer();
+
+    $types = AphrontHTTPParameterType::getAllTypes();
+
+    $table = id(new PhabricatorHTTPParameterTypeTableView())
+      ->setHTTPParameterTypes($types);
+
+    return id(new PHUIObjectBoxView())
+      ->setHeaderText(pht('HTTP Parameter Types'))
+      ->setTable($table);
+  }
+
+}
diff --git a/src/applications/config/view/PhabricatorHTTPParameterTypeTableView.php b/src/applications/config/view/PhabricatorHTTPParameterTypeTableView.php
new file mode 100644
--- /dev/null
+++ b/src/applications/config/view/PhabricatorHTTPParameterTypeTableView.php
@@ -0,0 +1,56 @@
+<?php
+
+final class PhabricatorHTTPParameterTypeTableView
+  extends AphrontView {
+
+  private $types;
+
+  public function setHTTPParameterTypes(array $types) {
+    assert_instances_of($types, 'AphrontHTTPParameterType');
+    $this->types = $types;
+    return $this;
+  }
+
+  public function getHTTPParameterTypes() {
+    return $this->types;
+  }
+
+  public function render() {
+    $types = $this->getHTTPParameterTypes();
+    $types = mpull($types, null, 'getTypeName');
+
+    $br = phutil_tag('br');
+
+    $rows = array();
+    foreach ($types as $name => $type) {
+      $formats = $type->getFormatDescriptions();
+      $formats = phutil_implode_html($br, $formats);
+
+      $examples = $type->getExamples();
+      $examples = phutil_implode_html($br, $examples);
+
+      $rows[] = array(
+        $name,
+        $formats,
+        $examples,
+      );
+    }
+
+    $table = id(new AphrontTableView($rows))
+      ->setHeaders(
+        array(
+          pht('Type'),
+          pht('Formats'),
+          pht('Examples'),
+        ))
+      ->setColumnClasses(
+        array(
+          'pri top',
+          'top',
+          'wide top prewrap',
+        ));
+
+    return $table;
+  }
+
+}
diff --git a/src/applications/transactions/editfield/PhabricatorEditField.php b/src/applications/transactions/editfield/PhabricatorEditField.php
--- a/src/applications/transactions/editfield/PhabricatorEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorEditField.php
@@ -194,35 +194,29 @@
   }
 
   protected function getValueExistsInSubmit(AphrontRequest $request, $key) {
-    return $request->getExists($key);
+    return $this->getHTTPParameterType()->getExists($request, $key);
   }
 
   protected function getValueFromSubmit(AphrontRequest $request, $key) {
-    return $request->getStr($key);
+    return $this->getHTTPParameterType()->getValue($request, $key);
   }
 
   protected function getDefaultValue() {
-    return null;
+    return $this->getHTTPParameterType()->getDefaultValue();
   }
 
-  protected function getListFromRequest(
-    AphrontRequest $request,
-    $key) {
+  final public function getHTTPParameterType() {
+    $type = $this->newHTTPParameterType();
 
-    $list = $request->getArr($key, null);
-    if ($list === null) {
-      $list = $request->getStrList($key);
+    if ($type) {
+      $type->setViewer($this->getViewer());
     }
 
-    if (!$list) {
-      return array();
-    }
-
-    return $list;
+    return $type;
   }
 
-  public function getHTTPParameterType() {
-    return 'string';
+  protected function newHTTPParameterType() {
+    return new AphrontStringHTTPParameterType();
   }
 
   public function setEditTypeKey($edit_type_key) {
@@ -290,7 +284,7 @@
       id(new PhabricatorSimpleEditType())
         ->setEditType($type_key)
         ->setTransactionType($transaction_type)
-        ->setValueType($this->getHTTPParameterType())
+        ->setValueType($this->getHTTPParameterType()->getTypeName())
         ->setDescription($this->getDescription())
         ->setMetadata($this->metadata),
     );
diff --git a/src/applications/transactions/editfield/PhabricatorPolicyEditField.php b/src/applications/transactions/editfield/PhabricatorPolicyEditField.php
--- a/src/applications/transactions/editfield/PhabricatorPolicyEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorPolicyEditField.php
@@ -51,8 +51,8 @@
     return $control;
   }
 
-  public function getHTTPParameterType() {
-    return 'phid';
+  protected function newHTTPParameterType() {
+    return new AphrontPHIDHTTPParameterType();
   }
 
 }
diff --git a/src/applications/transactions/editfield/PhabricatorSelectEditField.php b/src/applications/transactions/editfield/PhabricatorSelectEditField.php
--- a/src/applications/transactions/editfield/PhabricatorSelectEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorSelectEditField.php
@@ -22,8 +22,8 @@
       ->setOptions($this->getOptions());
   }
 
-  public function getHTTPParameterType() {
-    return 'select';
+  protected function newHTTPParameterType() {
+    return new AphrontSelectHTTPParameterType();
   }
 
 }
diff --git a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
--- a/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorSpaceEditField.php
@@ -9,8 +9,8 @@
     return null;
   }
 
-  public function getHTTPParameterType() {
-    return 'phid';
+  protected function newHTTPParameterType() {
+    return new AphrontPHIDHTTPParameterType();
   }
 
 }
diff --git a/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php b/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php
--- a/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php
+++ b/src/applications/transactions/editfield/PhabricatorTokenizerEditField.php
@@ -18,11 +18,6 @@
     return $control;
   }
 
-  public function setOriginalValue(array $value) {
-    $this->originalValue = $value;
-    return $this;
-  }
-
   public function setValue($value) {
     $this->originalValue = $value;
     return parent::setValue($value);
@@ -33,11 +28,7 @@
     // correctly is easier?
     $this->originalValue = $request->getArr($key.'.original');
 
-    return $this->getListFromRequest($request, $key);
-  }
-
-  protected function getDefaultValue() {
-    return array();
+    return parent::getValueFromSubmit($request, $key);
   }
 
   protected function getValueForTransaction() {
@@ -87,8 +78,8 @@
     return $new;
   }
 
-  public function getHTTPParameterType() {
-    return 'list<phid>';
+  protected function newHTTPParameterType() {
+    return new AphrontPHIDListHTTPParameterType();
   }
 
 }
diff --git a/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php b/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php
--- a/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php
+++ b/src/applications/transactions/view/PhabricatorApplicationEditHTTPParameterHelpView.php
@@ -43,7 +43,7 @@
       if ($type === null) {
         unset($fields[$key]);
       }
-      $types[$type][] = $field;
+      $types[$type->getTypeName()] = $type;
     }
 
     $intro = pht(<<<EOTEXT
@@ -95,7 +95,7 @@
       $rows[] = array(
         $field->getLabel(),
         $field->getKey(),
-        $field->getHTTPParameterType(),
+        $field->getHTTPParameterType()->getTypeName(),
         $field->getDescription(),
       );
     }
@@ -234,75 +234,8 @@
 EOTEXT
       );
 
-    // TODO: This should be formalized and modularized.
-    $type_spec = array(
-      'string' => array(
-        'format' => pht('URL encoded text.'),
-        'examples' => array(
-          'v=simple',
-          'v=properly%20escaped%20text',
-        ),
-      ),
-      'select' => array(
-        'format' => pht('Value from allowed set.'),
-        'examples' => array(
-          'v=value',
-        ),
-      ),
-      'list<phid>' => array(
-        'format' => array(
-          pht('Comma-separated list of PHIDs.'),
-          pht('List of PHIDs, as array.'),
-        ),
-        'examples' => array(
-          'v=PHID-XXXX-1111,PHID-XXXX-2222',
-          'v[]=PHID-XXXX-1111&v[]=PHID-XXXX-2222',
-        ),
-      ),
-      'phid' => array(
-        'format' => pht('Single PHID.'),
-        'examples' => pht('v=PHID-XXX-1111'),
-      ),
-    );
-
-    $rows = array();
-    $br = phutil_tag('br');
-    foreach ($types as $type => $fields) {
-      $spec = idx($type_spec, $type, array());
-
-      $field_list = mpull($fields, 'getKey');
-      $field_list = phutil_implode_html($br, $field_list);
-
-      $format_list = idx($spec, 'format', array());
-      $format_list = phutil_implode_html($br, (array)$format_list);
-
-      $example_list = idx($spec, 'examples', array());
-      $example_list = phutil_implode_html($br, (array)$example_list);
-
-      $rows[] = array(
-        $type,
-        $field_list,
-        $format_list,
-        $example_list,
-      );
-    }
-
-    $types_table = id(new AphrontTableView($rows))
-      ->setNoDataString(pht('This object has no fields with types.'))
-      ->setHeaders(
-        array(
-          pht('Type'),
-          pht('Fields'),
-          pht('Formats'),
-          pht('Examples'),
-        ))
-      ->setColumnClasses(
-        array(
-          'pri top',
-          'top',
-          'top',
-          'wide top prewrap',
-        ));
+    $types_table = id(new PhabricatorHTTPParameterTypeTableView())
+      ->setHTTPParameterTypes($types);
 
     return array(
       $this->renderInstructions($intro),