diff --git a/core/lib/Drupal/Core/Config/Config.php b/core/lib/Drupal/Core/Config/Config.php
index bdeeba1..b3b51c2 100644
--- a/core/lib/Drupal/Core/Config/Config.php
+++ b/core/lib/Drupal/Core/Config/Config.php
@@ -110,7 +110,7 @@ public function get($key = '') {
    * {@inheritdoc}
    */
   public function setData(array $data) {
-    $this->data = $data;
+    parent::setData($data);
     $this->resetOverriddenData();
     return $this;
   }
diff --git a/core/lib/Drupal/Core/Config/ConfigBase.php b/core/lib/Drupal/Core/Config/ConfigBase.php
index 3508611..37e0d80 100644
--- a/core/lib/Drupal/Core/Config/ConfigBase.php
+++ b/core/lib/Drupal/Core/Config/ConfigBase.php
@@ -160,13 +160,39 @@ public function get($key = '') {
    *
    * @return $this
    *   The configuration object.
+   *
+   * @throws \Drupal\Core\Config\ConfigValueException
+   *   If any key in $data in any depth contains a dot.
    */
   public function setData(array $data) {
+    $this->validateKeys($data);
     $this->data = $data;
     return $this;
   }
 
   /**
+   * Validate all keys in a passed in config array structure.
+   *
+   * @param array $data
+   *   Configuration array structure.
+   *
+   * @return null
+   *
+   * @throws \Drupal\Core\Config\ConfigValueException
+   *   If any key in $data in any depth contains a dot.
+   */
+  protected function validateKeys(array $data) {
+    foreach ($data as $key => $value) {
+      if (strpos($key, '.') !== FALSE) {
+        throw new ConfigValueException(String::format('@key key contains a dot which is not supported.', array('@key' => $key)));
+      }
+      if (is_array($value)) {
+        $this->validateKeys($value);
+      }
+    }
+  }
+
+  /**
    * Sets a value in this configuration object.
    *
    * @param string $key
@@ -176,10 +202,16 @@ public function setData(array $data) {
    *
    * @return $this
    *   The configuration object.
+   *
+   * @throws \Drupal\Core\Config\ConfigValueException
+   *   If $value is an array and any of its keys in any depth contains a dot.
    */
   public function set($key, $value) {
     // The dot/period is a reserved character; it may appear between keys, but
     // not within keys.
+    if (is_array($value)) {
+      $this->validateKeys($value);
+    }
     $parts = explode('.', $key);
     if (count($parts) == 1) {
       $this->data[$key] = $value;
diff --git a/core/lib/Drupal/Core/Config/ConfigValueException.php b/core/lib/Drupal/Core/Config/ConfigValueException.php
new file mode 100644
index 0000000..9802228
--- /dev/null
+++ b/core/lib/Drupal/Core/Config/ConfigValueException.php
@@ -0,0 +1,13 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\Core\Config\ConfigValueException.
+ */
+
+namespace Drupal\Core\Config;
+
+/**
+ * Exception thrown when config object values are invalid.
+ */
+class ConfigValueException extends ConfigException {}
diff --git a/core/modules/aggregator/src/Entity/Feed.php b/core/modules/aggregator/src/Entity/Feed.php
index 8c091c3..de3daed 100644
--- a/core/modules/aggregator/src/Entity/Feed.php
+++ b/core/modules/aggregator/src/Entity/Feed.php
@@ -13,6 +13,7 @@
 use Symfony\Component\DependencyInjection\Container;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\aggregator\FeedInterface;
+use Drupal\options\Plugin\Field\FieldType\ListItemBase;
 
 /**
  * Defines the aggregator feed entity class.
@@ -167,7 +168,7 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setDescription(t('The length of time between feed updates. Requires a correctly configured <a href="@cron">cron maintenance task</a>.', array('@cron' => url('admin/reports/status'))))
       ->setSetting('unsigned', TRUE)
       ->setRequired(TRUE)
-      ->setSetting('allowed_values', $period)
+      ->setSetting('allowed_values', ListItemBase::structureAllowedValues($period))
       ->setDisplayOptions('form', array(
         'type' => 'options_select',
         'weight' => -2,
diff --git a/core/modules/config/src/Tests/ConfigCRUDTest.php b/core/modules/config/src/Tests/ConfigCRUDTest.php
index dc6de36..5358c06 100644
--- a/core/modules/config/src/Tests/ConfigCRUDTest.php
+++ b/core/modules/config/src/Tests/ConfigCRUDTest.php
@@ -9,6 +9,7 @@
 
 use Drupal\Component\Utility\String;
 use Drupal\Core\Config\ConfigNameException;
+use Drupal\Core\Config\ConfigValueException;
 use Drupal\Core\Config\InstallStorage;
 use Drupal\simpletest\DrupalUnitTestBase;
 use Drupal\Core\Config\FileStorage;
@@ -190,6 +191,31 @@ function testNameValidation() {
   }
 
   /**
+   * Tests the validation of configuration object values.
+   */
+  function testValueValidation() {
+    // Verify that setData() will catch dotted keys.
+    $message = 'Expected ConfigValueException was thrown from setData() for value with dotted keys.';
+    try {
+      \Drupal::config('namespace.object')->setData(array('key.value' => 12))->save();
+      $this->fail($message);
+    }
+    catch (ConfigValueException $e) {
+      $this->pass($message);
+    }
+
+    // Verify that set() will catch dotted keys.
+    $message = 'Expected ConfigValueException was thrown from set() for value with dotted keys.';
+    try {
+      \Drupal::config('namespace.object')->set('foo', array('key.value' => 12))->save();
+      $this->fail($message);
+    }
+    catch (ConfigValueException $e) {
+      $this->pass($message);
+    }
+  }
+
+  /**
    * Tests data type handling.
    */
   public function testDataTypes() {
diff --git a/core/modules/field/src/Tests/FormTest.php b/core/modules/field/src/Tests/FormTest.php
index 255c19d..c4bbf47 100644
--- a/core/modules/field/src/Tests/FormTest.php
+++ b/core/modules/field/src/Tests/FormTest.php
@@ -358,7 +358,7 @@ function testFieldFormMultivalueWithRequiredRadio() {
       'entity_type' => 'entity_test',
       'type' => 'list_text',
       'settings' => array(
-        'allowed_values' => array('yes' => 'yes', 'no' => 'no'),
+        'allowed_values' => array(array('value' => 'yes', 'label' => 'yes'), array('value' => 'no', 'label' => 'no')),
       ),
     ))->save();
     $instance = array(
diff --git a/core/modules/forum/config/install/field.field.taxonomy_term.forum_container.yml b/core/modules/forum/config/install/field.field.taxonomy_term.forum_container.yml
index a666915..d9f5631 100644
--- a/core/modules/forum/config/install/field.field.taxonomy_term.forum_container.yml
+++ b/core/modules/forum/config/install/field.field.taxonomy_term.forum_container.yml
@@ -4,9 +4,7 @@ langcode: en
 name: forum_container
 type: list_boolean
 settings:
-  allowed_values:
-    - ''
-    - ''
+  allowed_values: {  }
   allowed_values_function: ''
 module: options
 entity_type: taxonomy_term
diff --git a/core/modules/options/config/schema/options.schema.yml b/core/modules/options/config/schema/options.schema.yml
index 8c90839..b81080e 100644
--- a/core/modules/options/config/schema/options.schema.yml
+++ b/core/modules/options/config/schema/options.schema.yml
@@ -8,8 +8,15 @@ field.list_integer.settings:
       type: sequence
       label: 'Allowed values list'
       sequence:
-        - type: string
-          label: 'Value'
+        - type: mapping
+          label: 'Allowed value with label'
+          mapping:
+            value:
+              type: integer
+              label: 'Value'
+            label:
+              type: label
+              label: 'Label'
     allowed_values_function:
       type: string
       label: 'Allowed values function'
@@ -35,8 +42,18 @@ field.list_float.settings:
   label: 'List (float) settings'
   mapping:
     allowed_values:
-      type: ignore
+      type: sequence
       label: 'Allowed values list'
+      sequence:
+        - type: mapping
+          label: 'Allowed value with label'
+          mapping:
+            value:
+              type: float
+              label: 'Value'
+            label:
+              type: label
+              label: 'Label'
     allowed_values_function:
       type: string
       label: 'Allowed values function'
@@ -65,8 +82,15 @@ field.list_text.settings:
       type: sequence
       label: 'Allowed values list'
       sequence:
-        - type: string
-          label: 'Value'
+        - type: mapping
+          label: 'Allowed value with label'
+          mapping:
+            value:
+              type: string
+              label: 'Value'
+            label:
+              type: label
+              label: 'Label'
     allowed_values_function:
       type: string
       label: 'Allowed values function'
@@ -95,8 +119,15 @@ field.list_boolean.settings:
       type: sequence
       label: 'Allowed values list'
       sequence:
-        - type: string
-          label: 'Value'
+        - type: mapping
+          label: 'Allowed value with label'
+          mapping:
+            value:
+              type: boolean
+              label: 'Value'
+            label:
+              type: label
+              label: 'Label'
     allowed_values_function:
       type: string
       label: 'Allowed values function'
diff --git a/core/modules/options/options.module b/core/modules/options/options.module
index d9a1b24..d0b6c00 100644
--- a/core/modules/options/options.module
+++ b/core/modules/options/options.module
@@ -11,6 +11,7 @@
 use Drupal\Core\Entity\Exception\FieldStorageDefinitionUpdateForbiddenException;
 use Drupal\field\FieldConfigInterface;
 use Drupal\field\FieldConfigUpdateForbiddenException;
+use Drupal\options\Plugin\Field\FieldType\ListItemBase;
 
 /**
  * Implements hook_help().
@@ -60,8 +61,9 @@ function options_field_config_delete(FieldConfigInterface $field) {
  *   The entity object.
  *
  * @return
- *   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.
+ *   The array of allowed values. Each array element is an associative
+ *   array with 'value' and 'label' keys. The 'value' key stores the raw stored
+ *   value (number or text), the 'label' key stores the display labels.
  */
 function options_allowed_values(FieldDefinitionInterface $field_definition, EntityInterface $entity) {
   $allowed_values = &drupal_static(__FUNCTION__, array());
@@ -99,7 +101,11 @@ function options_field_config_update_forbid(FieldConfigInterface $field, FieldCo
     // Forbid any update that removes allowed values with actual data.
     $allowed_values = $field->getSetting('allowed_values');
     $prior_allowed_values = $prior_field->getSetting('allowed_values');
-    $lost_keys = array_diff(array_keys($prior_allowed_values), array_keys($allowed_values));
+    $type_definition = \Drupal::typedDataManager()->getDefinition('field_item:' . $field->getType());
+    $lost_keys = array_diff(
+      array_keys($type_definition['class']::simplifyAllowedValues($prior_allowed_values)),
+      array_keys($type_definition['class']::simplifyAllowedValues($allowed_values))
+    );
     if (_options_values_in_use($field->entity_type, $field->getName(), $lost_keys)) {
       throw new FieldStorageDefinitionUpdateForbiddenException(t('A list field (@field_name) with existing data cannot have its keys changed.', array('@field_name' => $field->getName())));
     }
diff --git a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
index 5c0b0d2..c7ee86d 100644
--- a/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
+++ b/core/modules/options/src/Plugin/Field/FieldFormatter/OptionsDefaultFormatter.php
@@ -7,8 +7,12 @@
 
 namespace Drupal\options\Plugin\Field\FieldFormatter;
 
+use Drupal\Core\Field\FieldDefinitionInterface;
 use Drupal\Core\Field\FormatterBase;
 use Drupal\Core\Field\FieldItemListInterface;
+use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
+use Drupal\Core\TypedData\TypedDataManager;
+use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Plugin implementation of the 'list_default' formatter.
@@ -24,7 +28,48 @@
  *   }
  * )
  */
-class OptionsDefaultFormatter extends FormatterBase {
+class OptionsDefaultFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+    return new static(
+      $plugin_id,
+      $plugin_definition,
+      $configuration['field_definition'],
+      $configuration['settings'],
+      $configuration['label'],
+      $configuration['view_mode'],
+      $configuration['third_party_settings'],
+      $container->get('typed_data_manager')
+    );
+  }
+
+  /**
+   * Constructs a new OptionsDefaultFormatter.
+   *
+   * @param string $plugin_id
+   *   The plugin_id for the formatter.
+   * @param mixed $plugin_definition
+   *   The plugin implementation definition.
+   * @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
+   *   The definition of the field to which the formatter is associated.
+   * @param array $settings
+   *   The formatter settings.
+   * @param string $label
+   *   The formatter label display setting.
+   * @param string $view_mode
+   *   The view mode.
+   * @param array $third_party_settings
+   *   Third party settings.
+   * @param \Drupal\Core\TypedData\TypedDataManager $typed_data_manager
+   *   The typed data manager.
+   */
+  public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, TypedDataManager $typed_data_manager) {
+    parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
+    $this->typedDataManager = $typed_data_manager;
+  }
 
   /**
    * {@inheritdoc}
@@ -33,7 +78,8 @@ public function viewElements(FieldItemListInterface $items) {
     $elements = array();
 
     $entity = $items->getEntity();
-    $allowed_values = options_allowed_values($this->fieldDefinition, $entity);
+    $type_definition = $this->typedDataManager->getDefinition('field_item:' . $this->fieldDefinition->getType());
+    $allowed_values = $type_definition['class']::simplifyAllowedValues(options_allowed_values($this->fieldDefinition, $entity));
 
     foreach ($items as $delta => $item) {
       if (isset($allowed_values[$item->value])) {
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListBooleanItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListBooleanItem.php
index 5d16585..6446017 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListBooleanItem.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListBooleanItem.php
@@ -27,6 +27,8 @@
  */
 class ListBooleanItem extends FieldItemBase implements AllowedValuesInterface {
 
+  use ListItemAllowedValuesTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -62,7 +64,7 @@ public function getSettableValues(AccountInterface $account = NULL) {
    * {@inheritdoc}
    */
   public function getSettableOptions(AccountInterface $account = NULL) {
-    return options_allowed_values($this->getFieldDefinition(), $this->getEntity());
+    return static::simplifyAllowedValues(options_allowed_values($this->getFieldDefinition(), $this->getEntity()));
   }
 
   /**
@@ -107,8 +109,8 @@ public function settingsForm(array &$form, array &$form_state, $has_data) {
     $allowed_values_function = $this->getSetting('allowed_values_function');
 
     $values = $allowed_values;
-    $off_value = array_shift($values);
-    $on_value = array_shift($values);
+    $off_item = array_shift($values);
+    $on_item = array_shift($values);
 
     $element['allowed_values'] = array(
       '#type' => 'value',
@@ -119,7 +121,7 @@ public function settingsForm(array &$form, array &$form_state, $has_data) {
     $element['allowed_values']['on'] = array(
       '#type' => 'textfield',
       '#title' => t('On value'),
-      '#default_value' => $on_value,
+      '#default_value' => $on_item['label'],
       '#required' => FALSE,
       '#description' => t('If left empty, "1" will be used.'),
       // Change #parents to make sure the element is not saved into field
@@ -129,7 +131,7 @@ public function settingsForm(array &$form, array &$form_state, $has_data) {
     $element['allowed_values']['off'] = array(
       '#type' => 'textfield',
       '#title' => t('Off value'),
-      '#default_value' => $off_value,
+      '#default_value' => $off_item['label'],
       '#required' => FALSE,
       '#description' => t('If left empty, "0" will be used.'),
       // Change #parents to make sure the element is not saved into field
@@ -160,7 +162,7 @@ public function settingsForm(array &$form, array &$form_state, $has_data) {
   public static function optionsBooleanAllowedValues($element, $input, $form_state) {
     $on = NestedArray::getValue($form_state['input'], $element['#on_parents']);
     $off = NestedArray::getValue($form_state['input'], $element['#off_parents']);
-    return array($off, $on);
+    return array(array('value' => false, 'label' => $off), array('value' => true, 'label' => $on));
   }
 
 }
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php
index 30cd382..195e1c7 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListFloatItem.php
@@ -69,16 +69,13 @@ protected function allowedValuesDescription() {
   protected static function extractAllowedValues($string, $has_data) {
     $values = parent::extractAllowedValues($string, $has_data);
     if ($values) {
-      $keys = array_keys($values);
-      $labels = array_values($values);
-      $keys = array_map(function ($key) {
+      foreach ($values as $item) {
         // Float keys are represented as strings and need to be disambiguated
         // ('.5' is '0.5').
-        return is_numeric($key) ? (string) (float) $key : $key;
-      }, $keys);
-
-      return array_combine($keys, $labels);
+        $item['value'] = is_numeric($item['value']) ? (string) (float) $item['value'] : $item['value'];
+      }
     }
+    return $values;
   }
 
   /**
@@ -90,4 +87,24 @@ protected static function validateAllowedValue($option) {
     }
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public static function simplifyAllowedValues(array $structured_values) {
+    $values = array();
+    foreach ($structured_values as $item) {
+      // Nested elements are embedded in the label.
+      // See ListItemBase::structureAllowedValues().
+      if (is_array($item['label'])) {
+        $item['label'] = static::simplifyAllowedValues($item['label']);
+      }
+      // Cast the value to a float first so that .5 and 0.5 are the same value
+      // and then cast to a string so that values like 0.5 can be used as array
+      // keys.
+      // @see http://php.net/manual/en/language.types.array.php
+      $values[(string) (float) $item['value']] = $item['label'];
+    }
+    return $values;
+  }
+
 }
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListItemAllowedValuesTrait.php b/core/modules/options/src/Plugin/Field/FieldType/ListItemAllowedValuesTrait.php
new file mode 100644
index 0000000..d10e303
--- /dev/null
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemAllowedValuesTrait.php
@@ -0,0 +1,64 @@
+<?php
+
+/**
+ * @file
+ * Contains \Drupal\options\Plugin\Field\FieldType\ListItemAllowedValuesTrait.
+ */
+
+namespace Drupal\options\Plugin\Field\FieldType;
+
+/**
+ * Provides a trait for handling list item allowed values.
+ */
+trait ListItemAllowedValuesTrait {
+
+  /**
+   * Simplify allowed values to a key-value array from the structured array.
+   *
+   * @param array $structured_values
+   *   Array of items with a 'value' and 'label' key each for the allowed
+   *   values.
+   *
+   * @return array
+   *   Allowed values were the array key is the 'value' value, the value is
+   *   the 'label' value.
+   */
+  public static function simplifyAllowedValues(array $structured_values) {
+    $values = array();
+    foreach ($structured_values as $item) {
+      if (is_array($item['label'])) {
+        // Nested elements are embedded in the label.
+        // See structureAllowedValues().
+        $item['label'] = static::simplifyAllowedValues($item['label']);
+      }
+      $values[$item['value']] = $item['label'];
+    }
+    return $values;
+  }
+
+  /**
+   * Creates a structured array of allowed values from a key-value array.
+   *
+   * @param array $values
+   *   Allowed values were the array key is the 'value' value, the value is
+   *   the 'label' value.
+   *
+   * @return array
+   *   Array of items with a 'value' and 'label' key each for the allowed
+   *   values.
+   */
+  public static function structureAllowedValues(array $values) {
+    $structured_values = array();
+    foreach ($values as $value => $label) {
+      if (is_array($label)) {
+        $label = static::structureAllowedValues($label);
+      }
+      $structured_values[] = array(
+        'value' => $value,
+        'label' => $label,
+      );
+    }
+    return $structured_values;
+  }
+
+}
diff --git a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
index d61aef1..194b3f5 100644
--- a/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
+++ b/core/modules/options/src/Plugin/Field/FieldType/ListItemBase.php
@@ -17,6 +17,8 @@
  */
 abstract class ListItemBase extends FieldItemBase implements AllowedValuesInterface {
 
+  use ListItemAllowedValuesTrait;
+
   /**
    * {@inheritdoc}
    */
@@ -59,7 +61,7 @@ public function getSettableValues(AccountInterface $account = NULL) {
    */
   public function getSettableOptions(AccountInterface $account = NULL) {
     $allowed_options = options_allowed_values($this->getFieldDefinition(), $this->getEntity());
-    return $allowed_options;
+    return static::simplifyAllowedValues($allowed_options);
   }
 
   /**
@@ -129,16 +131,25 @@ public static function validateAllowedValues($element, &$form_state) {
     }
     else {
       // Check that keys are valid for the field type.
-      foreach ($values as $key => $value) {
-        if ($error = static::validateAllowedValue($key)) {
+      $keys_in_use = array();
+      foreach ($values as $item) {
+        if ($error = static::validateAllowedValue($item['value'])) {
           \Drupal::formBuilder()->setError($element, $form_state, $error);
           break;
         }
+        if (in_array($item['value'], $keys_in_use)) {
+          \Drupal::formBuilder()->setError($element, $form_state, t('One key can only be used once.'));
+          break;
+        }
+        $keys_in_use[] = $item['value'];
       }
 
       // Prevent removing values currently in use.
       if ($element['#field_has_data']) {
-        $lost_keys = array_diff(array_keys($element['#allowed_values']), array_keys($values));
+        $lost_keys = array_diff(
+          array_keys(static::simplifyAllowedValues($element['#allowed_values'])),
+          array_keys(static::simplifyAllowedValues($values))
+        );
         if (_options_values_in_use($element['#entity_type'], $element['#field_name'], $lost_keys)) {
           \Drupal::formBuilder()->setError($element, $form_state, t('Allowed values list: some values are being removed while currently in use.'));
         }
@@ -193,7 +204,7 @@ protected static function extractAllowedValues($string, $has_data) {
         return;
       }
 
-      $values[$key] = $value;
+      $values[] = array('value' => $key, 'label' => $value);
     }
 
     // We generate keys only if the list contains no explicit key at all.
@@ -220,19 +231,19 @@ protected static function validateAllowedValue($option) { }
    *
    * This string format is suitable for edition in a textarea.
    *
-   * @param array $values
-   *   An array of values, where array keys are values and array values are
-   *   labels.
+   * @param array $structured_values
+   *   An array of values, where elements are arrays with 'value' and 'label'
+   *   keys.
    *
    * @return string
    *   The string representation of the $values array:
    *    - Values are separated by a carriage return.
-   *    - Each value is in the format "value|label" or "value".
+   *    - Each value is in the format "value|label".
    */
-  protected function allowedValuesString($values) {
+  protected function allowedValuesString($structured_values) {
     $lines = array();
-    foreach ($values as $key => $value) {
-      $lines[] = "$key|$value";
+    foreach ($structured_values as $item) {
+      $lines[] = $item['value'] . '|' . $item['label'];
     }
     return implode("\n", $lines);
   }
diff --git a/core/modules/options/src/Tests/OptionsFieldTest.php b/core/modules/options/src/Tests/OptionsFieldTest.php
index ed775b4..aad59bf 100644
--- a/core/modules/options/src/Tests/OptionsFieldTest.php
+++ b/core/modules/options/src/Tests/OptionsFieldTest.php
@@ -45,7 +45,7 @@ function testUpdateAllowedValues() {
     $entity = entity_create('entity_test');
     $entity->{$this->fieldName}->value = 1;
     $entity->save();
-    $this->field->settings['allowed_values'] = array(2 => 'Two');
+    $this->field->settings['allowed_values'] = array(array('value' => 2, 'label' => 'Two'));
     try {
       $this->field->save();
       $this->fail(t('Cannot update a list field to not include keys with existing data.'));
@@ -58,7 +58,7 @@ function testUpdateAllowedValues() {
     $entity->save();
 
     // Removed options do not appear.
-    $this->field->settings['allowed_values'] = array(2 => 'Two');
+    $this->field->settings['allowed_values'] = array(array('value' => 2, 'label' => 'Two'));
     $this->field->save();
     $entity = entity_create('entity_test');
     $form = \Drupal::service('entity.form_builder')->getForm($entity);
@@ -67,7 +67,7 @@ function testUpdateAllowedValues() {
     $this->assertTrue(empty($form[$this->fieldName]['widget'][3]), 'Option 3 does not exist');
 
     // Completely new options appear.
-    $this->field->settings['allowed_values'] = array(10 => 'Update', 20 => 'Twenty');
+    $this->field->settings['allowed_values'] = array(array('value' => 10, 'label' => 'Update'), array('value' => 20, 'label' => 'Twenty'));
     $this->field->save();
     // The entity holds an outdated field object with the old allowed values
     // setting, so we need to reintialize the entity object.
diff --git a/core/modules/options/src/Tests/OptionsFieldUITest.php b/core/modules/options/src/Tests/OptionsFieldUITest.php
index baecfca..bf01335 100644
--- a/core/modules/options/src/Tests/OptionsFieldUITest.php
+++ b/core/modules/options/src/Tests/OptionsFieldUITest.php
@@ -60,15 +60,24 @@ function testOptionsAllowedValuesInteger() {
 
     // Flat list of textual values.
     $string = "Zero\nOne";
-    $array = array('0' => 'Zero', '1' => 'One');
+    $array = array(
+      array('value' => 0, 'label' => 'Zero'),
+      array('value' => 1, 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
     // Explicit integer keys.
     $string = "0|Zero\n2|Two";
-    $array = array('0' => 'Zero', '2' => 'Two');
+    $array = array(
+      array('value' => 0, 'label' => 'Zero'),
+      array('value' => 2, 'label' => 'Two'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.');
     // Check that values can be added and removed.
     $string = "0|Zero\n1|One";
-    $array = array('0' => 'Zero', '1' => 'One');
+    $array = array(
+      array('value' => 0, 'label' => 'Zero'),
+      array('value' => 1, 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
     // Non-integer keys.
     $this->assertAllowedValuesInput("1.1|One", 'keys must be integers', 'Non integer keys are rejected.');
@@ -88,18 +97,31 @@ function testOptionsAllowedValuesInteger() {
 
     // Check that values can be added but values in use cannot be removed.
     $string = "0|Zero\n1|One\n2|Two";
-    $array = array('0' => 'Zero', '1' => 'One', '2' => 'Two');
+    $array = array(
+      array('value' => 0, 'label' => 'Zero'),
+      array('value' => 1, 'label' => 'One'),
+      array('value' => 2, 'label' => 'Two'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values can be added.');
     $string = "0|Zero\n1|One";
-    $array = array('0' => 'Zero', '1' => 'One');
+    $array = array(
+      array('value' => 0, 'label' => 'Zero'),
+      array('value' => 1, 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
     $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
 
     // Delete the node, remove the value.
     $node->delete();
     $string = "0|Zero";
-    $array = array('0' => 'Zero');
+    $array = array(
+      array('value' => 0, 'label' => 'Zero'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
+
+    // Check that the same key can only be used once.
+    $string = "0|Zero\n0|One";
+    $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Same value cannot be used multiple times.');
   }
 
   /**
@@ -111,15 +133,25 @@ function testOptionsAllowedValuesFloat() {
 
     // Flat list of textual values.
     $string = "Zero\nOne";
-    $array = array('0' => 'Zero', '1' => 'One');
+    $array = array(
+      array('value' => (float) 0, 'label' => 'Zero'),
+      array('value' => (float) 1, 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
     // Explicit numeric keys.
     $string = "0|Zero\n.5|Point five";
-    $array = array('0' => 'Zero', '0.5' => 'Point five');
+    $array = array(
+      array('value' => (float) 0, 'label' => 'Zero'),
+      array('value' => (float) 0.5, 'label' => 'Point five'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Integer keys are accepted.');
     // Check that values can be added and removed.
     $string = "0|Zero\n.5|Point five\n1.0|One";
-    $array = array('0' => 'Zero', '0.5' => 'Point five', '1' => 'One');
+    $array = array(
+      array('value' => (float) 0, 'label' => 'Zero'),
+      array('value' => (float) 0.5, 'label' => 'Point five'),
+      array('value' => (float) 1, 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
     // Non-numeric keys.
     $this->assertAllowedValuesInput("abc|abc\n", 'each key must be a valid integer or decimal', 'Non numeric keys are rejected.');
@@ -129,7 +161,7 @@ function testOptionsAllowedValuesFloat() {
     // Create a node with actual data for the field.
     $settings = array(
       'type' => $this->type,
-      $this->field_name => array(array('value' => .5)),
+      $this->field_name => array(array('value' => (float) 0.5)),
     );
     $node = $this->drupalCreateNode($settings);
 
@@ -138,18 +170,35 @@ function testOptionsAllowedValuesFloat() {
 
     // Check that values can be added but values in use cannot be removed.
     $string = "0|Zero\n.5|Point five\n2|Two";
-    $array = array('0' => 'Zero', '0.5' => 'Point five', '2' => 'Two');
+    $array = array(
+      array('value' => (float) 0, 'label' => 'Zero'),
+      array('value' => (float) 0.5, 'label' => 'Point five'),
+      array('value' => (float) 2, 'label' => 'Two'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values can be added.');
     $string = "0|Zero\n.5|Point five";
-    $array = array('0' => 'Zero', '0.5' => 'Point five');
+    $array = array(
+      array('value' => (float) 0, 'label' => 'Zero'),
+      array('value' => (float) 0.5, 'label' => 'Point five'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
     $this->assertAllowedValuesInput("0|Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
 
     // Delete the node, remove the value.
     $node->delete();
     $string = "0|Zero";
-    $array = array('0' => 'Zero');
+    $array = array(
+      array('value' => (float) 0, 'label' => 'Zero'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
+
+    // Check that the same key can only be used once.
+    $string = "0.5|Point five\n0.5|Half";
+    $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Same value cannot be used multiple times.');
+
+    // Check that different forms of the same float value cannot be used.
+    $string = "0|Zero\n.5|Point five\n0.5|Half";
+    $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Different forms of the same value cannot be used.');
   }
 
   /**
@@ -161,19 +210,31 @@ function testOptionsAllowedValuesText() {
 
     // Flat list of textual values.
     $string = "Zero\nOne";
-    $array = array('Zero' => 'Zero', 'One' => 'One');
+    $array = array(
+      array('value' => 'Zero', 'label' => 'Zero'),
+      array('value' => 'One', 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are accepted.');
     // Explicit keys.
     $string = "zero|Zero\none|One";
-    $array = array('zero' => 'Zero', 'one' => 'One');
+    $array = array(
+      array('value' => 'zero', 'label' => 'Zero'),
+      array('value' => 'one', 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted.');
     // Check that values can be added and removed.
     $string = "zero|Zero\ntwo|Two";
-    $array = array('zero' => 'Zero', 'two' => 'Two');
+    $array = array(
+      array('value' => 'zero', 'label' => 'Zero'),
+      array('value' => 'two', 'label' => 'Two'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values can be added and removed.');
     // Mixed list of keyed and unkeyed values.
     $string = "zero|Zero\nOne\n";
-    $array = array('zero' => 'Zero', 'One' => 'One');
+    $array = array(
+      array('value' => 'zero', 'label' => 'Zero'),
+      array('value' => 'One', 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Mixed lists are accepted.');
     // Overly long keys.
     $this->assertAllowedValuesInput("zero|Zero\n" . $this->randomName(256) . "|One", 'each key must be a string at most 255 characters long', 'Overly long keys are rejected.');
@@ -188,23 +249,48 @@ function testOptionsAllowedValuesText() {
     // Check that flat lists of values are still accepted once the field has
     // data.
     $string = "Zero\nOne";
-    $array = array('Zero' => 'Zero', 'One' => 'One');
+    $array = array(
+      array('value' => 'Zero', 'label' => 'Zero'),
+      array('value' => 'One', 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Unkeyed lists are still accepted once the field has data.');
 
     // Check that values can be added but values in use cannot be removed.
     $string = "Zero\nOne\nTwo";
-    $array = array('Zero' => 'Zero', 'One' => 'One', 'Two' => 'Two');
+    $array = array(
+      array('value' => 'Zero', 'label' => 'Zero'),
+      array('value' => 'One', 'label' => 'One'),
+      array('value' => 'Two', 'label' => 'Two'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values can be added.');
     $string = "Zero\nOne";
-    $array = array('Zero' => 'Zero', 'One' => 'One');
+    $array = array(
+      array('value' => 'Zero', 'label' => 'Zero'),
+      array('value' => 'One', 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
     $this->assertAllowedValuesInput("Zero", 'some values are being removed while currently in use', 'Values in use cannot be removed.');
 
     // Delete the node, remove the value.
     $node->delete();
     $string = "Zero";
-    $array = array('Zero' => 'Zero');
+    $array = array(
+      array('value' => 'Zero', 'label' => 'Zero'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Values not in use can be removed.');
+
+    // Check that string values with dots can be used.
+    $node->delete();
+    $string = "Zero\nexample.com|Example";
+    $array = array(
+      array('value' => 'Zero', 'label' => 'Zero'),
+      array('value' => 'example.com', 'label' => 'Example'),
+    );
+    $this->assertAllowedValuesInput($string, $array, 'String value with dot is supported.');
+
+    // Check that the same key can only be used once.
+    $string = "Zero\nZero";
+    $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Same value cannot be used multiple times.');
   }
 
   /**
@@ -217,7 +303,10 @@ function testOptionsAllowedValuesBoolean() {
     // Check that the separate 'On' and 'Off' form fields work.
     $on = $this->randomName();
     $off = $this->randomName();
-    $allowed_values = array(1 => $on, 0 => $off);
+    $allowed_values = array(
+      array('value' => false, 'label' => $off),
+      array('value' => true, 'label' => $on),
+    );
     $edit = array(
       'on' => $on,
       'off' => $off,
@@ -244,8 +333,15 @@ function testOptionsTrimmedValuesText() {
 
     // Explicit keys.
     $string = "zero |Zero\none | One";
-    $array = array('zero' => 'Zero', 'one' => 'One');
+    $array = array(
+      array('value' => 'zero', 'label' => 'Zero'),
+      array('value' => 'one', 'label' => 'One'),
+    );
     $this->assertAllowedValuesInput($string, $array, 'Explicit keys are accepted and trimmed.');
+
+    // Check that the same key can only be used once.
+    $string = " Zero  \n   Zero  ";
+    $this->assertAllowedValuesInput($string, 'One key can only be used once.', 'Same value cannot be used multiple times.');
   }
 
   /**
diff --git a/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php b/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php
index 272e026..2c97d2e 100644
--- a/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php
+++ b/core/modules/options/src/Tests/OptionsFieldUnitTestBase.php
@@ -63,7 +63,11 @@ public function setUp() {
       'type' => 'list_integer',
       'cardinality' => 1,
       'settings' => array(
-        'allowed_values' => array(1 => 'One', 2 => 'Two', 3 => 'Three'),
+        'allowed_values' => array(
+          array('value' => 1, 'label' => 'One'),
+          array('value' => 2, 'label' => 'Two'),
+          array('value' => 3, 'label' => 'Three')
+        ),
       ),
     );
     $this->field = entity_create('field_config', $this->fieldDefinition);
diff --git a/core/modules/options/src/Tests/OptionsWidgetsTest.php b/core/modules/options/src/Tests/OptionsWidgetsTest.php
index bcec12c..05bc1e5 100644
--- a/core/modules/options/src/Tests/OptionsWidgetsTest.php
+++ b/core/modules/options/src/Tests/OptionsWidgetsTest.php
@@ -70,12 +70,12 @@ function setUp() {
       'settings' => array(
         'allowed_values' => array(
           // Make sure that 0 works as an option.
-          0 => 'Zero',
-          1 => 'One',
+          array('value' => 0, 'label' => 'Zero'),
+          array('value' => 1, 'label' => 'One'),
           // Make sure that option text is properly sanitized.
-          2 => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>',
+          array('value' => 2, 'label' => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>'),
           // Make sure that HTML entities in option text are not double-encoded.
-          3 => 'Some HTML encoded markup with &lt; &amp; &gt;',
+          array('value' => 3, 'label' => 'Some HTML encoded markup with &lt; &amp; &gt;'),
         ),
       ),
     ));
@@ -90,10 +90,10 @@ function setUp() {
       'settings' => array(
         'allowed_values' => array(
           // Make sure that 0 works as an option.
-          0 => 'Zero',
-          1 => 'One',
+          array('value' => 0, 'label' => 'Zero'),
+          array('value' => 1, 'label' => 'One'),
           // Make sure that option text is properly sanitized.
-          2 => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>',
+          array('value' => 2, 'label' => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>'),
         ),
       ),
     ));
@@ -108,9 +108,9 @@ function setUp() {
       'settings' => array(
         'allowed_values' => array(
           // Make sure that 0 works as an option.
-          0 => 'Zero',
+          array('value' => 0, 'label' => 'Zero'),
           // Make sure that option text is properly sanitized.
-          1 => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>',
+          array('value' => 1, 'label' => 'Some <script>dangerous</script> & unescaped <strong>markup</strong>'),
         ),
       ),
     ));
@@ -170,7 +170,7 @@ function testRadioButtons() {
     $this->assertFieldValues($entity_init, 'card_1', array());
 
     // Check that required radios with one option is auto-selected.
-    $this->card_1->settings['allowed_values'] = array(99 => 'Only allowed value');
+    $this->card_1->settings['allowed_values'] = array(array('value' => 99, 'label' => 'Only allowed value'));
     $this->card_1->save();
     $instance->required = TRUE;
     $instance->save();
@@ -259,7 +259,7 @@ function testCheckBoxes() {
     $this->assertFieldValues($entity_init, 'card_2', array());
 
     // Required checkbox with one option is auto-selected.
-    $this->card_2->settings['allowed_values'] = array(99 => 'Only allowed value');
+    $this->card_2->settings['allowed_values'] = array(array('value' => 99, 'label' => 'Only allowed value'));
     $this->card_2->save();
     $instance->required = TRUE;
     $instance->save();
diff --git a/core/modules/options/tests/options_test.module b/core/modules/options/tests/options_test.module
index 1416fa0..ba56132 100644
--- a/core/modules/options/tests/options_test.module
+++ b/core/modules/options/tests/options_test.module
@@ -7,6 +7,7 @@
 
 use Drupal\Core\Entity\EntityInterface;
 use Drupal\Core\Field\FieldDefinitionInterface;
+use Drupal\options\Plugin\Field\FieldType\ListItemBase;
 
 /**
  * Allowed values callback.
@@ -22,7 +23,7 @@ function options_test_allowed_values_callback(FieldDefinitionInterface $field_de
     ),
   );
 
-  return $values;
+  return ListItemBase::structureAllowedValues($values);
 }
 
 /**
@@ -37,5 +38,5 @@ function options_test_dynamic_values_callback(FieldDefinitionInterface $field_de
     $entity->bundle(),
   );
   // We need the values of the entity as keys.
-  return array_combine($values, $values);
+  return ListItemBase::structureAllowedValues(array_combine($values, $values));
 }
