diff --git a/core/modules/options/options.module b/core/modules/options/options.module
index ae6760d..da181ef 100644
--- a/core/modules/options/options.module
+++ b/core/modules/options/options.module
@@ -6,8 +6,10 @@
  */
 
 use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldItemListInterface;
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
-use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 use Drupal\Core\Routing\RouteMatchInterface;
 use Drupal\field\FieldStorageConfigInterface;
 
@@ -54,30 +56,41 @@ function options_field_storage_config_delete(FieldStorageConfigInterface $field_
  * sanitized through \Drupal\Core\Field\AllowedTagsXssTrait::fieldFilterXss()
  * before being displayed.
  *
- * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
- *   The field definition.
- * @param \Drupal\Core\Entity\EntityInterface $entity
- *   The entity object.
+ * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $definition
+ *   The field storage definition.
+ * @param \Drupal\Core\Entity\FieldableEntityInterface|NULL $entity
+ *   (optional) If called from the context of a specific field on a specific
+ *   entity, then this is the entity. This allows custom
+ *   'allowed_values_function' functions to either restrict the values or
+ *   customize the labels for particular bundles and entities. Note that when
+ *   configuring Views filters, this is not passed, so it is recommended that
+ *   such functions are written such that the  array keys returned when this
+ *   value is NULL is the superset of what is returned when it is provided.
  *
- * @return
+ * @return array
  *   The array of allowed values. Keys of the array are the raw stored values
  *   (number or text), values of the array are the display labels.
  */
-function options_allowed_values(FieldDefinitionInterface $field_definition, EntityInterface $entity) {
+function options_allowed_values(FieldStorageDefinitionInterface $definition, FieldableEntityInterface $entity = NULL) {
   $allowed_values = &drupal_static(__FUNCTION__, array());
 
-  $cache_id = implode(':', array($entity->getEntityTypeId(), $entity->bundle(), $field_definition->getName()));
+  $cache_keys = array($definition->getTargetEntityTypeId(), $definition->getName());
+  if ($entity) {
+    $cache_keys[] = 'entity';
+  }
+  $cache_id = implode(':', $cache_keys);
+
   if (!isset($allowed_values[$cache_id])) {
-    $function = $field_definition->getSetting('allowed_values_function');
+    $function = $definition->getSetting('allowed_values_function');
     // If $cacheable is FALSE, then the allowed values are not statically
     // cached. See options_test_dynamic_values_callback() for an example of
     // generating dynamic and uncached values.
     $cacheable = TRUE;
     if (!empty($function)) {
-      $values = $function($field_definition, $entity, $cacheable);
+      $values = $function($definition, $entity, $cacheable);
     }
     else {
-      $values = $field_definition->getSetting('allowed_values');
+      $values = $definition->getSetting('allowed_values');
     }
 
     if ($cacheable) {
diff --git a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
index 61fb8b1..0754c53 100644
--- a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
+++ b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
@@ -10,6 +10,7 @@
 use Drupal\Core\Field\AllowedTagsXssTrait;
 use Drupal\Core\Field\FormatterBase;
 use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\Plugin\Field\FieldWidget\OptionsWidgetBase;
 
 /**
  * Plugin implementation of the 'list_default' formatter.
@@ -34,21 +35,41 @@ class OptionsDefaultFormatter extends FormatterBase {
   public function viewElements(FieldItemListInterface $items) {
     $elements = array();
 
-    $entity = $items->getEntity();
-    $allowed_values = options_allowed_values($this->fieldDefinition, $entity);
-
-    foreach ($items as $delta => $item) {
-      if (isset($allowed_values[$item->value])) {
-        $output = $this->fieldFilterXss($allowed_values[$item->value]);
+    // Only collect allowed options if there are actually items to display.
+    if ($items->count()) {
+      $provider = $items->getFieldDefinition()
+        ->getFieldStorageDefinition()
+        ->getOptionsProvider('value', $items->getEntity());
+      // Flatten the possible options, to support opt groups.
+      $options = $this->flattenOptions($provider->getPossibleOptions());
+
+      foreach ($items as $delta => $item) {
+        $value = $item->value;
+        // If the stored value is in the current set of allowed values, display
+        // the associated label, otherwise just display the raw value.
+        $output = isset($options[$value]) ? $options[$value] : $value;
+        $elements[$delta] = array('#markup' => $this->fieldFilterXss($output));
       }
-      else {
-        // If no match was found in allowed values, fall back to the key.
-        $output = $this->fieldFilterXss($item->value);
-      }
-      $elements[$delta] = array('#markup' => $output);
     }
 
     return $elements;
   }
 
+  /**
+   * Flattens an array of allowed values.
+   *
+   * @param array $array
+   *   A single or multidimensional array.
+   *
+   * @return array
+   *   The flattened array.
+   *
+   * @todo Remove it once https://www.drupal.org/node/2392301 landed.
+   */
+  protected function flattenOptions(array $array) {
+    $result = array();
+    array_walk_recursive($array, function($a, $b) use (&$result) { $result[$b] = $a; });
+    return $result;
+  }
+
 }
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
index 19c4294..bdf063a 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
@@ -63,7 +63,7 @@ public function getSettableValues(AccountInterface $account = NULL) {
    * {@inheritdoc}
    */
   public function getSettableOptions(AccountInterface $account = NULL) {
-    $allowed_options = options_allowed_values($this->getFieldDefinition(), $this->getEntity());
+    $allowed_options = options_allowed_values($this->getFieldDefinition()->getFieldStorageDefinition(), $this->getEntity());
     return $allowed_options;
   }
 
diff --git a/core/modules/options/src/Tests/OptionsDynamicValuesApiTest.php b/core/modules/options/src/Tests/OptionsDynamicValuesApiTest.php
new file mode 100644
index 0000000..e1df82d
--- /dev/null
+++ b/core/modules/options/src/Tests/OptionsDynamicValuesApiTest.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\options\Tests\OptionsDynamicValuesApiTest.
+ */
+
+namespace Drupal\options\Tests;
+
+/**
+* Tests the options allowed values api.
+ *
+ * @group options
+*/
+class OptionsDynamicValuesApiTest extends OptionsDynamicValuesTestBase {
+
+  /**
+   * Tests options_allowed_values().
+   *
+   * @see options_test_dynamic_values_callback()
+   */
+  public function testOptionsAllowedValues() {
+    // Test allowed values without passed $items.
+    $values = options_allowed_values($this->fieldStorage);
+    $this->assertEqual([], $values);
+
+    $values = options_allowed_values($this->fieldStorage, $this->entity);
+
+    $expected_values = array(
+      $this->entity->label(),
+      $this->entity->url(),
+      $this->entity->uuid(),
+      $this->entity->bundle(),
+    );
+    $expected_values = array_combine($expected_values, $expected_values);
+    $this->assertEqual($expected_values, $values);
+  }
+
+}
diff --git a/core/modules/options/src/Tests/OptionsDynamicValuesTestBase.php b/core/modules/options/src/Tests/OptionsDynamicValuesTestBase.php
index 43d0bc9..20f49fd 100644
--- a/core/modules/options/src/Tests/OptionsDynamicValuesTestBase.php
+++ b/core/modules/options/src/Tests/OptionsDynamicValuesTestBase.php
@@ -28,11 +28,18 @@
    */
   protected $entity;
 
+  /**
+   * The field storage.
+   *
+   * @var \Drupal\Core\Field\FieldStorageDefinitionInterface
+   */
+  protected $fieldStorage;
+
   protected function setUp() {
     parent::setUp();
 
     $this->field_name = 'test_options';
-    entity_create('field_storage_config', array(
+    $this->fieldStorage = entity_create('field_storage_config', array(
       'field_name' => $this->field_name,
       'entity_type' => 'entity_test_rev',
       'type' => 'list_string',
@@ -40,7 +47,9 @@ protected function setUp() {
       'settings' => array(
         'allowed_values_function' => 'options_test_dynamic_values_callback',
       ),
-    ))->save();
+    ));
+    $this->fieldStorage->save();
+
     $this->field = entity_create('field_config', array(
       'field_name' => $this->field_name,
       'entity_type' => 'entity_test_rev',
diff --git a/core/modules/options/tests/options_test/options_test.module b/core/modules/options/tests/options_test/options_test.module
index bfd4315..fe4861a 100644
--- a/core/modules/options/tests/options_test/options_test.module
+++ b/core/modules/options/tests/options_test/options_test.module
@@ -5,13 +5,16 @@
  * Helper module for the List module tests.
  */
 
-use Drupal\Core\Entity\EntityInterface;
-use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
 
 /**
  * Allowed values callback.
+ *
+ * @see options_allowed_values().
  */
-function options_test_allowed_values_callback(FieldDefinitionInterface $field_definition, EntityInterface $entity) {
+function options_test_allowed_values_callback(FieldStorageDefinitionInterface $definition, FieldableEntityInterface $entity) {
   $values = array(
     'Group 1' => array(
       0 => 'Zero',
@@ -30,15 +33,25 @@ function options_test_allowed_values_callback(FieldDefinitionInterface $field_de
 
 /**
  * An entity-bound allowed values callback.
+ *
+ * @todo This function violates the recommendation in options_allowed_values()
+ *   to return the superset of values when $items is NULL. Since this is not
+ *   yet used for testing Views integration, that is ok. Fix this in
+ *   https://www.drupal.org/node/2012130.
+ *
+ * @see options_allowed_values().
  */
-function options_test_dynamic_values_callback(FieldDefinitionInterface $field_definition, EntityInterface $entity, &$cacheable) {
-  $cacheable = FALSE;
-  $values = array(
-    $entity->label(),
-    $entity->url(),
-    $entity->uuid(),
-    $entity->bundle(),
-  );
+function options_test_dynamic_values_callback(FieldStorageDefinitionInterface $definition, FieldableEntityInterface $entity = NULL, &$cacheable = NULL) {
+  $values = array();
+  if (isset($entity)) {
+    $cacheable = FALSE;
+    $values = array(
+      $entity->label(),
+      $entity->url(),
+      $entity->uuid(),
+      $entity->bundle(),
+    );
+  }
   // We need the values of the entity as keys.
   return array_combine($values, $values);
 }
