Repository: mozilla-services/react-jsonschema-form
Pull request #978 (2018-07-12 13:59:14)

Add hidden widget for arrays
### Reasons for making this change

Add 'hidden' widget for arrays. Fixes https://github.com/mozilla-services/react-jsonschema-form/issues/977


### Checklist

* [X ] **I'm adding a new feature**



Patch:
diff --git a/README.md b/README.md
index b59790607..12e4260d0 100644
--- a/README.md
+++ b/README.md
@@ -60,7 +60,7 @@ A [live playground](https://mozilla-services.github.io/react-jsonschema-form/) i
      - [Field labels](#field-labels)
      - [HTML5 Input Types](#html5-input-types)
      - [Form attributes](#form-attributes)
-     - [Form disable](#form-disable)
+     - [Disabling a form](#disabling-a-form)
   - [Advanced customization](#advanced-customization)
      - [Field template](#field-template)
      - [Array Field Template](#array-field-template)
@@ -512,7 +512,7 @@ const uiSchema = {
 
 #### `expandable` option
 
-If `additionalProperties` contains a schema object, an add button for new properies is shown by default. The UX for editing properties whose names are user-defined is still experimental.
+If `additionalProperties` contains a schema object, an add button for new properties is shown by default. The UX for editing properties whose names are user-defined is still experimental.
 
 You can turn support for `additionalProperties` off with the `expandable` option in `uiSchema`:
 
@@ -659,6 +659,49 @@ This will be rendered as follows:
 </select>
 ```
 
+This also works for radio buttons:
+
+```js
+const schema = {
+  "type": "boolean",
+  "oneOf": [
+    {
+      "const": true,
+      "title": "Yes"
+    },
+    {
+      "const": false,
+      "title": "No"
+    }
+  ]
+};
+
+const uiSchema = {
+  "ui:widget": "radio"
+};
+```
+
+This will be rendered as follows:
+
+```html
+<div class="field-radio-group">
+  <div class="radio">
+    <label>
+      <span>
+        <input type="radio" name="0.005549338200675935" value="true"><span>Enable</span>
+      </span>
+    </label>
+  </div>
+  <div class="radio">
+    <label>
+      <span>
+        <input type="radio" name="0.005549338200675935" value="false"><span>Disable</span>
+      </span>
+    </label>
+  </div>
+</div>
+```
+
 A live example of both approaches side-by-side can be found in the **Alternatives** tab of the [playground](https://mozilla-services.github.io/react-jsonschema-form/).
 
 ### Disabled attribute for `enum` fields
@@ -903,9 +946,9 @@ The `Form` component supports the following html attributes:
   schema={} />
 ```
 
-### Form disable
+### Disabling a form
 
-Its possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down thru each field of the form. 
+It's possible to disable the whole form by setting the `disabled` prop. The `disabled` prop is then forwarded down to each field of the form. 
 
 ```jsx
 <Form
@@ -913,7 +956,7 @@ Its possible to disable the whole form by setting the `disabled` prop. The `disa
   schema={} />
 ```
 
-If you just want to disable some of the fields see the `ui:disabled` parameter in the uiSchema directive. 
+If you just want to disable some of the fields, see the [`ui:disabled`](#disabled-fields) parameter in the `uiSchema` directive. 
 
 ## Advanced customization
 
@@ -1063,7 +1106,9 @@ The following props are passed to each `ObjectFieldTemplate`:
 - `TitleField`: The `TitleField` from the registry (in case you wanted to utilize it).
 - `title`: A string value containing the title for the object.
 - `description`: A string value containing the description for the object.
+- `disabled`: A boolean value stating if the object is disabled.
 - `properties`: An array of object representing the properties in the array. Each of the properties represent a child with properties described below.
+- `readonly`: A boolean value stating if the object is read-only.
 - `required`: A boolean value stating if the object is required.
 - `schema`: The schema object for this object.
 - `uiSchema`: The uiSchema object for this object field.
@@ -1088,15 +1133,13 @@ An error list template is basically a React stateless component being passed err
 function ErrorListTemplate(props) {
   const {errors} = props;
   return (
-    <div>
-      {errors.map((error, i) => {
-        return (
-          <li key={i}>
+    <ul>
+      {errors.map(error => (
+          <li key={error.stack}>
             {error.stack}
           </li>
-        );
-      })}
-    </div>
+        ))}
+    </ul>
   );
 }
 
@@ -1261,7 +1304,7 @@ render((
 
 #### Customizing widgets text input
 
-All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individially, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration).
+All the widgets that render a text input use the `BaseInput` component internally. If you need to customize all text inputs without customizing all widgets individually, you can provide a `BaseInput` component in the `widgets` property of `Form` (see [Custom component registration](#custom-component-registration).
 
 ### Custom field components
 
@@ -1334,7 +1377,7 @@ A field component will always be passed the following props:
  - `formData`: The data for this field;
  - `errorSchema`: The tree of errors for this field and its children;
  - `registry`: A [registry](#the-registry-object) object (read next).
- - `formContext`: A [formContext](#the-formcontext-object) object (read next next).
+ - `formContext`: A [formContext](#the-formcontext-object) object (read next).
 
 #### The `registry` object
 
@@ -1792,8 +1835,6 @@ The JSON Schema standard says that the dependency is triggered if the property i
 
 In this example the user is prompted with different follow-up questions dynamically based on their answer to the first question.
 
-Note that this is quite far from complete `oneOf` support!
-
 In these examples, the "Do you have any pets?" question is validated against the corresponding property in each schema in the `oneOf` array. If exactly one matches, the rest of that schema is merged with the existing schema.
 
 ## JSON Schema supporting status
@@ -1801,10 +1842,14 @@ In these examples, the "Do you have any pets?" question is validated against the
 This component follows [JSON Schema](http://json-schema.org/documentation.html) specs. Due to the limitation of form widgets, there are some exceptions as follows:
 
 * `additionalItems` keyword for arrays
+
     This keyword works when `items` is an array. `additionalItems: true` is not supported because there's no widget to represent an item of any type. In this case it will be treated as no additional items allowed. `additionalItems` being a valid schema is supported.
+
 * `anyOf`, `allOf`, and `oneOf`, or multiple `types` (i.e. `"type": ["string", "array"]`
-    Nobody yet has come up with a PR that adds this feature with a simple and easy-to-understand UX.
-    You can use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data but this feature does not bring general support for `oneOf` elsewhere in a schema.
+
+    The `anyOf`  and `oneOf` keywords are supported,  however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`.
+
+    You can also use `oneOf` with [schema dependencies](#schema-dependencies) to dynamically add schema properties based on input data.
 
 ## Tips and tricks
 
@@ -1869,14 +1914,15 @@ $ git commit -m "Bump version $VERSION"
 $ git tag v$VERSION
 $ npm run dist
 $ npm publish
-$ git push --tags origin
+$ git push --tags origin master
 ```
 
 ## FAQ
 
 ### Q: Does rjsf support `oneOf`, `anyOf`, multiple types in an array, etc.?
 
-A: Not yet (except for a special case where you can use `oneOf` in [schema dependencies](#schema-dependencies)), but perhaps you will be the person whose PR will finally add the feature in a way that gets merged. For inspiration, see [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648).
+A: The `anyOf`  and `oneOf` keywords are supported,  however, properties declared inside the `anyOf/oneOf` should not overlap with properties "outside" of the `anyOf/oneOf`.
+There is also special cased where you can use `oneOf` in [schema dependencies](#schema-dependencies), If you'd like to help improve support for these keywords, see the following issues for inspiration [#329](https://github.com/mozilla-services/react-jsonschema-form/pull/329) or [#417](https://github.com/mozilla-services/react-jsonschema-form/pull/417). See also: [#52](https://github.com/mozilla-services/react-jsonschema-form/issues/52), [#151](https://github.com/mozilla-services/react-jsonschema-form/issues/151), [#171](https://github.com/mozilla-services/react-jsonschema-form/issues/171), [#200](https://github.com/mozilla-services/react-jsonschema-form/issues/200), [#282](https://github.com/mozilla-services/react-jsonschema-form/issues/282), [#302](https://github.com/mozilla-services/react-jsonschema-form/pull/302), [#330](https://github.com/mozilla-services/react-jsonschema-form/issues/330), [#430](https://github.com/mozilla-services/react-jsonschema-form/issues/430), [#522](https://github.com/mozilla-services/react-jsonschema-form/issues/522), [#538](https://github.com/mozilla-services/react-jsonschema-form/issues/538), [#551](https://github.com/mozilla-services/react-jsonschema-form/issues/551), [#552](https://github.com/mozilla-services/react-jsonschema-form/issues/552), or [#648](https://github.com/mozilla-services/react-jsonschema-form/issues/648).
 
 ### Q: Will react-jsonschema-form support Material, Ant-Design, Foundation, or [some other specific widget library or frontend style]?
 
diff --git a/package.json b/package.json
index 1db7ca591..79ecf38dc 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "react-jsonschema-form",
-  "version": "1.0.7",
+  "version": "1.2.0",
   "description": "A simple React component capable of building HTML forms out of a JSON schema.",
   "scripts": {
     "build:readme": "toctoc README.md -w",
diff --git a/playground/app.js b/playground/app.js
index bf4f868c3..2a8dffdb6 100644
--- a/playground/app.js
+++ b/playground/app.js
@@ -357,6 +357,9 @@ class App extends Component {
   load = data => {
     // Reset the ArrayFieldTemplate whenever you load new data
     const { ArrayFieldTemplate, ObjectFieldTemplate } = data;
+    // uiSchema is missing on some examples. Provide a default to
+    // clear the field in all cases.
+    const { uiSchema = {} } = data;
     // force resetting form component instance
     this.setState({ form: false }, _ =>
       this.setState({
@@ -364,6 +367,7 @@ class App extends Component {
         form: true,
         ArrayFieldTemplate,
         ObjectFieldTemplate,
+        uiSchema,
       })
     );
   };
diff --git a/playground/samples/alternatives.js b/playground/samples/alternatives.js
index cfdbc6e44..fd1647cfa 100644
--- a/playground/samples/alternatives.js
+++ b/playground/samples/alternatives.js
@@ -22,6 +22,20 @@ module.exports = {
           },
         ],
       },
+      Toggle: {
+        title: "Toggle",
+        type: "boolean",
+        oneOf: [
+          {
+            title: "Enable",
+            const: true,
+          },
+          {
+            title: "Disable",
+            const: false,
+          },
+        ],
+      },
     },
     title: "Image editor",
     type: "object",
@@ -39,6 +53,10 @@ module.exports = {
         },
         title: "Color mask",
       },
+      toggleMask: {
+        title: "Apply color mask",
+        $ref: "#/definitions/Toggle",
+      },
       colorPalette: {
         type: "array",
         title: "Color palette",
@@ -58,6 +76,9 @@ module.exports = {
     blendMode: {
       "ui:enumDisabled": ["multiply"],
     },
+    toggleMask: {
+      "ui:widget": "radio",
+    },
   },
   formData: {
     currentColor: "#00ff00",
diff --git a/playground/samples/anyOf.js b/playground/samples/anyOf.js
new file mode 100644
index 000000000..432b0e0be
--- /dev/null
+++ b/playground/samples/anyOf.js
@@ -0,0 +1,37 @@
+module.exports = {
+  schema: {
+    type: "object",
+    properties: {
+      age: {
+        type: "integer",
+        title: "Age",
+      },
+    },
+    anyOf: [
+      {
+        title: "First method of identification",
+        properties: {
+          firstName: {
+            type: "string",
+            title: "First name",
+            default: "Chuck",
+          },
+          lastName: {
+            type: "string",
+            title: "Last name",
+          },
+        },
+      },
+      {
+        title: "Second method of identification",
+        properties: {
+          idCode: {
+            type: "string",
+            title: "ID code",
+          },
+        },
+      },
+    ],
+  },
+  formData: {},
+};
diff --git a/playground/samples/index.js b/playground/samples/index.js
index b496c0fb6..4f5f9346e 100644
--- a/playground/samples/index.js
+++ b/playground/samples/index.js
@@ -1,4 +1,6 @@
 import arrays from "./arrays";
+import anyOf from "./anyOf";
+import oneOf from "./oneOf";
 import nested from "./nested";
 import numbers from "./numbers";
 import simple from "./simple";
@@ -40,4 +42,6 @@ export const samples = {
   "Property dependencies": propertyDependencies,
   "Schema dependencies": schemaDependencies,
   "Additional Properties": additionalProperties,
+  "Any Of": anyOf,
+  "One Of": oneOf,
 };
diff --git a/playground/samples/oneOf.js b/playground/samples/oneOf.js
new file mode 100644
index 000000000..4ed400491
--- /dev/null
+++ b/playground/samples/oneOf.js
@@ -0,0 +1,24 @@
+module.exports = {
+  schema: {
+    type: "object",
+    oneOf: [
+      {
+        properties: {
+          lorem: {
+            type: "string",
+          },
+        },
+        required: ["lorem"],
+      },
+      {
+        properties: {
+          ipsum: {
+            type: "string",
+          },
+        },
+        required: ["ipsum"],
+      },
+    ],
+  },
+  formData: {},
+};
diff --git a/src/components/fields/ArrayField.js b/src/components/fields/ArrayField.js
index a85bd6fa0..f60e5b86f 100644
--- a/src/components/fields/ArrayField.js
+++ b/src/components/fields/ArrayField.js
@@ -1,8 +1,8 @@
 import AddButton from "../AddButton";
 import IconButton from "../IconButton";
 import React, { Component } from "react";
-import PropTypes from "prop-types";
 import includes from "core-js/library/fn/array/includes";
+import * as types from "../../types";
 
 import UnsupportedField from "./UnsupportedField";
 import {
@@ -103,7 +103,7 @@ function DefaultArrayItem(props) {
 
 function DefaultFixedArrayFieldTemplate(props) {
   return (
-    <fieldset className={props.className}>
+    <fieldset className={props.className} id={props.idSchema.$id}>
       <ArrayFieldTitle
         key={`array-field-title-${props.idSchema.$id}`}
         TitleField={props.TitleField}
@@ -139,7 +139,7 @@ function DefaultFixedArrayFieldTemplate(props) {
 
 function DefaultNormalArrayFieldTemplate(props) {
   return (
-    <fieldset className={props.className}>
+    <fieldset className={props.className} id={props.idSchema.$id}>
       <ArrayFieldTitle
         key={`array-field-title-${props.idSchema.$id}`}
         TitleField={props.TitleField}
@@ -276,20 +276,19 @@ class ArrayField extends Component {
           }
         }
       }
-      onChange(
-        formData.map((item, i) => {
-          // i is string, index and newIndex are numbers,
-          // so using "==" to compare
-          if (i == newIndex) {
-            return formData[index];
-          } else if (i == index) {
-            return formData[newIndex];
-          } else {
-            return item;
-          }
-        }),
-        newErrorSchema
-      );
+
+      function reOrderArray() {
+        // Copy item
+        let newFormData = formData.slice();
+
+        // Moves item from index to newIndex
+        newFormData.splice(index, 1);
+        newFormData.splice(newIndex, 0, formData[index]);
+
+        return newFormData;
+      }
+
+      onChange(reOrderArray(), newErrorSchema);
     };
   };
 
@@ -661,34 +660,7 @@ class ArrayField extends Component {
 }
 
 if (process.env.NODE_ENV !== "production") {
-  ArrayField.propTypes = {
-    schema: PropTypes.object.isRequired,
-    uiSchema: PropTypes.shape({
-      "ui:options": PropTypes.shape({
-        addable: PropTypes.bool,
-        orderable: PropTypes.bool,
-        removable: PropTypes.bool,
-      }),
-    }),
-    idSchema: PropTypes.object,
-    errorSchema: PropTypes.object,
-    onChange: PropTypes.func.isRequired,
-    onBlur: PropTypes.func,
-    onFocus: PropTypes.func,
-    formData: PropTypes.array,
-    required: PropTypes.bool,
-    disabled: PropTypes.bool,
-    readonly: PropTypes.bool,
-    autofocus: PropTypes.bool,
-    registry: PropTypes.shape({
-      widgets: PropTypes.objectOf(
-        PropTypes.oneOfType([PropTypes.func, PropTypes.object])
-      ).isRequired,
-      fields: PropTypes.objectOf(PropTypes.func).isRequired,
-      definitions: PropTypes.object.isRequired,
-      formContext: PropTypes.object.isRequired,
-    }),
-  };
+  ArrayField.propTypes = types.fieldProps;
 }
 
 export default ArrayField;
diff --git a/src/components/fields/BooleanField.js b/src/components/fields/BooleanField.js
index 00014f28c..37f134ed2 100644
--- a/src/components/fields/BooleanField.js
+++ b/src/components/fields/BooleanField.js
@@ -1,5 +1,5 @@
 import React from "react";
-import PropTypes from "prop-types";
+import * as types from "../../types";
 
 import {
   getWidget,
@@ -29,10 +29,27 @@ function BooleanField(props) {
   const { widgets, formContext } = registry;
   const { widget = "checkbox", ...options } = getUiOptions(uiSchema);
   const Widget = getWidget(schema, widget, widgets);
-  const enumOptions = optionsList({
-    enum: [true, false],
-    enumNames: schema.enumNames || ["yes", "no"],
-  });
+
+  let enumOptions;
+
+  if (Array.isArray(schema.oneOf)) {
+    enumOptions = optionsList({
+      oneOf: schema.oneOf.map(option => ({
+        ...option,
+        title: option.title || (option.const === true ? "yes" : "no"),
+      })),
+    });
+  } else {
+    enumOptions = optionsList({
+      enum: schema.enum || [true, false],
+      enumNames:
+        schema.enumNames ||
+        (schema.enum && schema.enum[0] === false
+          ? ["no", "yes"]
+          : ["yes", "no"]),
+    });
+  }
+
   return (
     <Widget
       options={{ ...options, enumOptions }}
@@ -55,28 +72,7 @@ function BooleanField(props) {
 }
 
 if (process.env.NODE_ENV !== "production") {
-  BooleanField.propTypes = {
-    schema: PropTypes.object.isRequired,
-    uiSchema: PropTypes.object,
-    idSchema: PropTypes.object,
-    onChange: PropTypes.func.isRequired,
-    onFocus: PropTypes.func,
-    onBlur: PropTypes.func,
-    formData: PropTypes.bool,
-    required: PropTypes.bool,
-    disabled: PropTypes.bool,
-    readonly: PropTypes.bool,
-    autofocus: PropTypes.bool,
-    registry: PropTypes.shape({
-      widgets: PropTypes.objectOf(
-        PropTypes.oneOfType([PropTypes.func, PropTypes.object])
-      ).isRequired,
-      fields: PropTypes.objectOf(PropTypes.func).isRequired,
-      definitions: PropTypes.object.isRequired,
-      formContext: PropTypes.object.isRequired,
-    }),
-    rawErrors: PropTypes.arrayOf(PropTypes.string),
-  };
+  BooleanField.propTypes = types.fieldProps;
 }
 
 BooleanField.defaultProps = {
diff --git a/src/components/fields/MultiSchemaField.js b/src/components/fields/MultiSchemaField.js
new file mode 100644
index 000000000..e11b9434a
--- /dev/null
+++ b/src/components/fields/MultiSchemaField.js
@@ -0,0 +1,162 @@
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import * as types from "../../types";
+import { guessType } from "../../utils";
+import { isValid } from "../../validate";
+
+class AnyOfField extends Component {
+  constructor(props) {
+    super(props);
+
+    const { formData, options } = this.props;
+
+    this.state = {
+      selectedOption: this.getMatchingOption(formData, options),
+    };
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const matchingOption = this.getMatchingOption(
+      nextProps.formData,
+      nextProps.options
+    );
+
+    if (matchingOption === this.state.selectedOption) {
+      return;
+    }
+
+    this.setState({ selectedOption: matchingOption });
+  }
+
+  getMatchingOption(formData, options) {
+    for (let i = 0; i < options.length; i++) {
+      if (isValid(options[i], formData)) {
+        return i;
+      }
+    }
+
+    // If the form data matches none of the options, use the first option
+    return 0;
+  }
+
+  onOptionChange = event => {
+    const selectedOption = parseInt(event.target.value, 10);
+    const { formData, onChange, options } = this.props;
+
+    if (guessType(formData) === "object") {
+      const newFormData = Object.assign({}, formData);
+
+      const optionsToDiscard = options.slice();
+      optionsToDiscard.splice(selectedOption, 1);
+
+      // Discard any data added using other options
+      for (const option of optionsToDiscard) {
+        if (option.properties) {
+          for (const key in option.properties) {
+            if (newFormData.hasOwnProperty(key)) {
+              delete newFormData[key];
+            }
+          }
+        }
+      }
+
+      onChange(newFormData);
+    } else {
+      onChange(undefined);
+    }
+
+    this.setState({
+      selectedOption: parseInt(event.target.value, 10),
+    });
+  };
+
+  render() {
+    const {
+      baseType,
+      disabled,
+      errorSchema,
+      formData,
+      idPrefix,
+      idSchema,
+      onBlur,
+      onChange,
+      onFocus,
+      options,
+      registry,
+      safeRenderCompletion,
+      uiSchema,
+    } = this.props;
+
+    const _SchemaField = registry.fields.SchemaField;
+    const { selectedOption } = this.state;
+
+    const option = options[selectedOption] || null;
+    let optionSchema;
+
+    if (option) {
+      // If the subschema doesn't declare a type, infer the type from the
+      // parent schema
+      optionSchema = option.type
+        ? option
+        : Object.assign({}, option, { type: baseType });
+    }
+
+    return (
+      <div className="panel panel-default panel-body">
+        <div className="form-group">
+          <select
+            className="form-control"
+            onChange={this.onOptionChange}
+            value={selectedOption}
+            id={`${idSchema.$id}_anyof_select`}>
+            {options.map((option, index) => {
+              return (
+                <option key={index} value={index}>
+                  {option.title || `Option ${index + 1}`}
+                </option>
+              );
+            })}
+          </select>
+        </div>
+
+        {option !== null && (
+          <_SchemaField
+            schema={optionSchema}
+            uiSchema={uiSchema}
+            errorSchema={errorSchema}
+            idSchema={idSchema}
+            idPrefix={idPrefix}
+            formData={formData}
+            onChange={onChange}
+            onBlur={onBlur}
+            onFocus={onFocus}
+            registry={registry}
+            safeRenderCompletion={safeRenderCompletion}
+            disabled={disabled}
+          />
+        )}
+      </div>
+    );
+  }
+}
+
+AnyOfField.defaultProps = {
+  disabled: false,
+  errorSchema: {},
+  idSchema: {},
+  uiSchema: {},
+};
+
+if (process.env.NODE_ENV !== "production") {
+  AnyOfField.propTypes = {
+    options: PropTypes.arrayOf(PropTypes.object).isRequired,
+    baseType: PropTypes.string,
+    uiSchema: PropTypes.object,
+    idSchema: PropTypes.object,
+    formData: PropTypes.any,
+    errorSchema: PropTypes.object,
+    registry: types.registry.isRequired,
+  };
+}
+
+export default AnyOfField;
diff --git a/src/components/fields/NumberField.js b/src/components/fields/NumberField.js
index 30c297b19..1f7f7c112 100644
--- a/src/components/fields/NumberField.js
+++ b/src/components/fields/NumberField.js
@@ -1,6 +1,6 @@
 import React from "react";
-import PropTypes from "prop-types";
 
+import * as types from "../../types";
 import { asNumber } from "../../utils";
 
 function NumberField(props) {
@@ -14,15 +14,7 @@ function NumberField(props) {
 }
 
 if (process.env.NODE_ENV !== "production") {
-  NumberField.propTypes = {
-    schema: PropTypes.object.isRequired,
-    uiSchema: PropTypes.object,
-    idSchema: PropTypes.object,
-    onChange: PropTypes.func.isRequired,
-    formData: PropTypes.oneOfType([PropTypes.number, PropTypes.string]),
-    required: PropTypes.bool,
-    formContext: PropTypes.object.isRequired,
-  };
+  NumberField.propTypes = types.fieldProps;
 }
 
 NumberField.defaultProps = {
diff --git a/src/components/fields/ObjectField.js b/src/components/fields/ObjectField.js
index 982aa3aaf..9eb1732d8 100644
--- a/src/components/fields/ObjectField.js
+++ b/src/components/fields/ObjectField.js
@@ -1,6 +1,6 @@
 import AddButton from "../AddButton";
 import React, { Component } from "react";
-import PropTypes from "prop-types";
+import * as types from "../../types";
 
 import {
   orderProperties,
@@ -30,7 +30,7 @@ function DefaultObjectFieldTemplate(props) {
 
   const { TitleField, DescriptionField } = props;
   return (
-    <fieldset>
+    <fieldset id={props.idSchema.$id}>
       {(props.uiSchema["ui:title"] || props.title) && (
         <TitleField
           id={`${props.idSchema.$id}__title`}
@@ -82,22 +82,17 @@ class ObjectField extends Component {
 
   onPropertyChange = (name, addedByAdditionalProperties = false) => {
     return (value, errorSchema) => {
-      let newFormData;
-      //section below sets zero value of input field to empty string
-      //instead of undefined, so that value input in additionalProperties
-      //doesn't disappear when emptied
       if (!value && addedByAdditionalProperties) {
-        newFormData = {
-          ...this.props.formData,
-          [name]: "",
-        };
-      } else {
-        newFormData = {
-          ...this.props.formData,
-          [name]: value,
-        };
+        // Don't set value = undefined for fields added by
+        // additionalProperties. Doing so removes them from the
+        // formData, which causes them to completely disappear
+        // (including the input field for the property name). Unlike
+        // fields which are "mandated" by the schema, these fields can
+        // be set to undefined by clicking a "delete field" button, so
+        // set empty values to the empty string.
+        value = "";
       }
-
+      const newFormData = { ...this.props.formData, [name]: value };
       this.props.onChange(
         newFormData,
         errorSchema &&
@@ -109,7 +104,7 @@ class ObjectField extends Component {
     };
   };
 
-  onDropIndexClick = key => {
+  onDropPropertyClick = key => {
     return event => {
       event.preventDefault();
       const { onChange, formData } = this.props;
@@ -203,7 +198,7 @@ class ObjectField extends Component {
     const description = uiSchema["ui:description"] || schema.description;
     let orderedProperties;
     try {
-      const properties = Object.keys(schema.properties);
+      const properties = Object.keys(schema.properties || {});
       orderedProperties = orderProperties(properties, uiSchema["ui:order"]);
     } catch (err) {
       return (
@@ -250,7 +245,7 @@ class ObjectField extends Component {
               registry={registry}
               disabled={disabled}
               readonly={readonly}
-              onDropIndexClick={this.onDropIndexClick}
+              onDropPropertyClick={this.onDropPropertyClick}
             />
           ),
           name,
@@ -259,6 +254,8 @@ class ObjectField extends Component {
           required,
         };
       }),
+      readonly,
+      disabled,
       required,
       idSchema,
       uiSchema,
@@ -271,25 +268,7 @@ class ObjectField extends Component {
 }
 
 if (process.env.NODE_ENV !== "production") {
-  ObjectField.propTypes = {
-    schema: PropTypes.object.isRequired,
-    uiSchema: PropTypes.object,
-    errorSchema: PropTypes.object,
-    idSchema: PropTypes.object,
-    onChange: PropTypes.func.isRequired,
-    formData: PropTypes.object,
-    required: PropTypes.bool,
-    disabled: PropTypes.bool,
-    readonly: PropTypes.bool,
-    registry: PropTypes.shape({
-      widgets: PropTypes.objectOf(
-        PropTypes.oneOfType([PropTypes.func, PropTypes.object])
-      ).isRequired,
-      fields: PropTypes.objectOf(PropTypes.func).isRequired,
-      definitions: PropTypes.object.isRequired,
-      formContext: PropTypes.object.isRequired,
-    }),
-  };
+  ObjectField.propTypes = types.fieldProps;
 }
 
 export default ObjectField;
diff --git a/src/components/fields/SchemaField.js b/src/components/fields/SchemaField.js
index 1d10983c0..b0965678e 100644
--- a/src/components/fields/SchemaField.js
+++ b/src/components/fields/SchemaField.js
@@ -2,9 +2,11 @@ import { ADDITIONAL_PROPERTY_FLAG } from "../../utils";
 import IconButton from "../IconButton";
 import React from "react";
 import PropTypes from "prop-types";
+import * as types from "../../types";
 
 import {
   isMultiSelect,
+  isSelect,
   retrieveSchema,
   toIdSchema,
   getDefaultRegistry,
@@ -36,6 +38,13 @@ function getFieldComponent(schema, uiSchema, idSchema, fields) {
   }
 
   const componentName = COMPONENT_TYPES[getSchemaType(schema)];
+
+  // If the type is not defined and the schema uses 'anyOf' or 'oneOf', don't
+  // render a field and let the MultiSchemaField component handle the form display
+  if (!componentName && (schema.anyOf || schema.oneOf)) {
+    return () => null;
+  }
+
   return componentName in fields
     ? fields[componentName]
     : () => {
@@ -121,10 +130,10 @@ function DefaultTemplate(props) {
     required,
     displayLabel,
     onKeyChange,
-    onDropIndexClick,
+    onDropPropertyClick,
   } = props;
   if (hidden) {
-    return children;
+    return <div className="hidden">{children}</div>;
   }
 
   const additional = props.schema.hasOwnProperty(ADDITIONAL_PROPERTY_FLAG);
@@ -164,7 +173,7 @@ function DefaultTemplate(props) {
               tabIndex="-1"
               style={{ border: "0" }}
               disabled={props.disabled || props.readonly}
-              onClick={onDropIndexClick(props.label)}
+              onClick={onDropPropertyClick(props.label)}
             />
           )}
         </div>
@@ -208,7 +217,7 @@ function SchemaFieldRender(props) {
     idPrefix,
     name,
     onKeyChange,
-    onDropIndexClick,
+    onDropPropertyClick,
     required,
     registry = getDefaultRegistry(),
   } = props;
@@ -308,7 +317,7 @@ function SchemaFieldRender(props) {
     label,
     hidden,
     onKeyChange,
-    onDropIndexClick,
+    onDropPropertyClick,
     required,
     disabled,
     readonly,
@@ -320,7 +329,55 @@ function SchemaFieldRender(props) {
     uiSchema,
   };
 
-  return <FieldTemplate {...fieldProps}>{field}</FieldTemplate>;
+  const _AnyOfField = registry.fields.AnyOfField;
+  const _OneOfField = registry.fields.OneOfField;
+
+  return (
+    <FieldTemplate {...fieldProps}>
+      {field}
+
+      {/*
+        If the schema `anyOf` or 'oneOf' can be rendered as a select control, don't
+        render the selection and let `StringField` component handle
+        rendering
+      */}
+      {schema.anyOf && !isSelect(schema) && (
+        <_AnyOfField
+          disabled={disabled}
+          errorSchema={errorSchema}
+          formData={formData}
+          idPrefix={idPrefix}
+          idSchema={idSchema}
+          onBlur={props.onBlur}
+          onChange={props.onChange}
+          onFocus={props.onFocus}
+          options={schema.anyOf}
+          baseType={schema.type}
+          registry={registry}
+          safeRenderCompletion={props.safeRenderCompletion}
+          uiSchema={uiSchema}
+        />
+      )}
+
+      {schema.oneOf && !isSelect(schema) && (
+        <_OneOfField
+          disabled={disabled}
+          errorSchema={errorSchema}
+          formData={formData}
+          idPrefix={idPrefix}
+          idSchema={idSchema}
+          onBlur={props.onBlur}
+          onChange={props.onChange}
+          onFocus={props.onFocus}
+          options={schema.oneOf}
+          baseType={schema.type}
+          registry={registry}
+          safeRenderCompletion={props.safeRenderCompletion}
+          uiSchema={uiSchema}
+        />
+      )}
+    </FieldTemplate>
+  );
 }
 
 class SchemaField extends React.Component {
@@ -354,17 +411,7 @@ if (process.env.NODE_ENV !== "production") {
     idSchema: PropTypes.object,
     formData: PropTypes.any,
     errorSchema: PropTypes.object,
-    registry: PropTypes.shape({
-      widgets: PropTypes.objectOf(
-        PropTypes.oneOfType([PropTypes.func, PropTypes.object])
-      ).isRequired,
-      fields: PropTypes.objectOf(PropTypes.func).isRequired,
-      definitions: PropTypes.object.isRequired,
-      ArrayFieldTemplate: PropTypes.func,
-      ObjectFieldTemplate: PropTypes.func,
-      FieldTemplate: PropTypes.func,
-      formContext: PropTypes.object.isRequired,
-    }),
+    registry: types.registry.isRequired,
   };
 }
 
diff --git a/src/components/fields/StringField.js b/src/components/fields/StringField.js
index 6ef3f3cbf..31dc31580 100644
--- a/src/components/fields/StringField.js
+++ b/src/components/fields/StringField.js
@@ -1,5 +1,5 @@
 import React from "react";
-import PropTypes from "prop-types";
+import * as types from "../../types";
 
 import {
   getWidget,
@@ -57,33 +57,7 @@ function StringField(props) {
 }
 
 if (process.env.NODE_ENV !== "production") {
-  StringField.propTypes = {
-    schema: PropTypes.object.isRequired,
-    uiSchema: PropTypes.object.isRequired,
-    idSchema: PropTypes.object,
-    onChange: PropTypes.func.isRequired,
-    onBlur: PropTypes.func,
-    onFocus: PropTypes.func,
-    formData: PropTypes.oneOfType([
-      PropTypes.string,
-      PropTypes.number,
-      PropTypes.bool,
-    ]),
-    registry: PropTypes.shape({
-      widgets: PropTypes.objectOf(
-        PropTypes.oneOfType([PropTypes.func, PropTypes.object])
-      ).isRequired,
-      fields: PropTypes.objectOf(PropTypes.func).isRequired,
-      definitions: PropTypes.object.isRequired,
-      formContext: PropTypes.object.isRequired,
-    }),
-    formContext: PropTypes.object.isRequired,
-    required: PropTypes.bool,
-    disabled: PropTypes.bool,
-    readonly: PropTypes.bool,
-    autofocus: PropTypes.bool,
-    rawErrors: PropTypes.arrayOf(PropTypes.string),
-  };
+  StringField.propTypes = types.fieldProps;
 }
 
 StringField.defaultProps = {
diff --git a/src/components/fields/index.js b/src/components/fields/index.js
index c8e92e5dd..758cb322a 100644
--- a/src/components/fields/index.js
+++ b/src/components/fields/index.js
@@ -1,6 +1,7 @@
 import ArrayField from "./ArrayField";
 import BooleanField from "./BooleanField";
 import DescriptionField from "./DescriptionField";
+import MultiSchemaField from "./MultiSchemaField";
 import NumberField from "./NumberField";
 import ObjectField from "./ObjectField";
 import SchemaField from "./SchemaField";
@@ -9,11 +10,13 @@ import TitleField from "./TitleField";
 import UnsupportedField from "./UnsupportedField";
 
 export default {
+  AnyOfField: MultiSchemaField,
   ArrayField,
   BooleanField,
   DescriptionField,
   NumberField,
   ObjectField,
+  OneOfField: MultiSchemaField,
   SchemaField,
   StringField,
   TitleField,
diff --git a/src/types.js b/src/types.js
new file mode 100644
index 000000000..4d536df70
--- /dev/null
+++ b/src/types.js
@@ -0,0 +1,36 @@
+import PropTypes from "prop-types";
+
+export const registry = PropTypes.shape({
+  ArrayFieldTemplate: PropTypes.func,
+  FieldTemplate: PropTypes.func,
+  ObjectFieldTemplate: PropTypes.func,
+  definitions: PropTypes.object.isRequired,
+  fields: PropTypes.objectOf(PropTypes.func).isRequired,
+  formContext: PropTypes.object.isRequired,
+  widgets: PropTypes.objectOf(
+    PropTypes.oneOfType([PropTypes.func, PropTypes.object])
+  ).isRequired,
+});
+
+export const fieldProps = {
+  autofocus: PropTypes.bool,
+  disabled: PropTypes.bool,
+  errorSchema: PropTypes.object,
+  formData: PropTypes.any,
+  idSchema: PropTypes.object,
+  onBlur: PropTypes.func,
+  onChange: PropTypes.func.isRequired,
+  onFocus: PropTypes.func,
+  rawErrors: PropTypes.arrayOf(PropTypes.string),
+  readonly: PropTypes.bool,
+  registry: registry.isRequired,
+  required: PropTypes.bool,
+  schema: PropTypes.object.isRequired,
+  uiSchema: PropTypes.shape({
+    "ui:options": PropTypes.shape({
+      addable: PropTypes.bool,
+      orderable: PropTypes.bool,
+      removable: PropTypes.bool,
+    }),
+  }),
+};
diff --git a/src/utils.js b/src/utils.js
index 47856324d..929294274 100644
--- a/src/utils.js
+++ b/src/utils.js
@@ -52,6 +52,7 @@ const widgetMap = {
     select: "SelectWidget",
     checkboxes: "CheckboxesWidget",
     files: "FileWidget",
+    hidden: "HiddenWidget",
   },
 };
 
@@ -157,9 +158,12 @@ function computeDefaults(schema, parentDefaults, definitions = {}) {
           if (schema.minItems > defaultsLength) {
             const defaultEntries = defaults || [];
             // populate the array with the defaults
+            const fillerSchema = Array.isArray(schema.items)
+              ? schema.additionalItems
+              : schema.items;
             const fillerEntries = fill(
               new Array(schema.minItems - defaultsLength),
-              computeDefaults(schema.items, schema.items.defaults, definitions)
+              computeDefaults(fillerSchema, fillerSchema.defaults, definitions)
             );
             // then fill up the rest with either the item default or empty, up to minItems
 
diff --git a/src/validate.js b/src/validate.js
index 3c8273e7a..222b16328 100644
--- a/src/validate.js
+++ b/src/validate.js
@@ -162,6 +162,8 @@ export default function validateFormData(
   }
 
   let errors = transformAjvErrors(ajv.errors);
+  // Clear errors to prevent persistent errors, see #1104
+  ajv.errors = null;
 
   if (typeof transformErrors === "function") {
     errors = transformErrors(errors);
@@ -182,3 +184,16 @@ export default function validateFormData(
 
   return { errors: newErrors, errorSchema: newErrorSchema };
 }
+
+/**
+ * Validates data against a schema, returning true if the data is valid, or
+ * false otherwise. If the schema is invalid, then this function will return
+ * false.
+ */
+export function isValid(schema, data) {
+  try {
+    return ajv.validate(schema, data);
+  } catch (e) {
+    return false;
+  }
+}


Test patch:
diff --git a/test/ArrayField_test.js b/test/ArrayField_test.js
index 40e7f90b3..299b67090 100644
--- a/test/ArrayField_test.js
+++ b/test/ArrayField_test.js
@@ -41,7 +41,9 @@ describe("ArrayField", () => {
     it("should render a fieldset", () => {
       const { node } = createFormComponent({ schema });
 
-      expect(node.querySelectorAll("fieldset")).to.have.length.of(1);
+      const fieldset = node.querySelectorAll("fieldset");
+      expect(fieldset).to.have.length.of(1);
+      expect(fieldset[0].id).eql("root");
     });
 
     it("should render a fieldset legend", () => {
@@ -270,6 +272,50 @@ describe("ArrayField", () => {
       expect(inputs[2].value).eql("bar");
     });
 
+    it("should move from first to last in the list", () => {
+      function moveAnywhereArrayItemTemplate(props) {
+        const buttons = [];
+        for (let i = 0; i < 3; i++) {
+          buttons.push(
+            <button
+              key={i}
+              className={"array-item-move-to-" + i}
+              onClick={props.onReorderClick(props.index, i)}>
+              {"Move item to index " + i}
+            </button>
+          );
+        }
+        return (
+          <div key={props.index} className={"item-" + props.index}>
+            {props.children}
+            {buttons}
+          </div>
+        );
+      }
+
+      function moveAnywhereArrayFieldTemplate(props) {
+        return (
+          <div className="array">
+            {props.items.map(moveAnywhereArrayItemTemplate)}
+          </div>
+        );
+      }
+
+      const { node } = createFormComponent({
+        schema,
+        formData: ["foo", "bar", "baz"],
+        ArrayFieldTemplate: moveAnywhereArrayFieldTemplate,
+      });
+
+      const button = node.querySelector(".item-0 .array-item-move-to-2");
+      Simulate.click(button);
+
+      const inputs = node.querySelectorAll(".field-string input[type=text]");
+      expect(inputs[0].value).eql("bar");
+      expect(inputs[1].value).eql("baz");
+      expect(inputs[2].value).eql("foo");
+    });
+
     it("should disable move buttons on the ends of the list", () => {
       const { node } = createFormComponent({
         schema,
diff --git a/test/BooleanField_test.js b/test/BooleanField_test.js
index eaa8a5a98..fe90f7420 100644
--- a/test/BooleanField_test.js
+++ b/test/BooleanField_test.js
@@ -107,6 +107,39 @@ describe("BooleanField", () => {
     expect(node.querySelector(".field input").checked).eql(true);
   });
 
+  it("should have default enum option labels for radio widgets", () => {
+    const { node } = createFormComponent({
+      schema: {
+        type: "boolean",
+      },
+      formData: true,
+      uiSchema: { "ui:widget": "radio" },
+    });
+
+    const labels = [].map.call(
+      node.querySelectorAll(".field-radio-group label"),
+      label => label.textContent
+    );
+    expect(labels).eql(["yes", "no"]);
+  });
+
+  it("should support enum option ordering for radio widgets", () => {
+    const { node } = createFormComponent({
+      schema: {
+        type: "boolean",
+        enum: [false, true],
+      },
+      formData: true,
+      uiSchema: { "ui:widget": "radio" },
+    });
+
+    const labels = [].map.call(
+      node.querySelectorAll(".field-radio-group label"),
+      label => label.textContent
+    );
+    expect(labels).eql(["no", "yes"]);
+  });
+
   it("should support enumNames for radio widgets", () => {
     const { node } = createFormComponent({
       schema: {
@@ -124,6 +157,58 @@ describe("BooleanField", () => {
     expect(labels).eql(["Yes", "No"]);
   });
 
+  it("should support oneOf titles for radio widgets", () => {
+    const { node } = createFormComponent({
+      schema: {
+        type: "boolean",
+        oneOf: [
+          {
+            const: true,
+            title: "Yes",
+          },
+          {
+            const: false,
+            title: "No",
+          },
+        ],
+      },
+      formData: true,
+      uiSchema: { "ui:widget": "radio" },
+    });
+
+    const labels = [].map.call(
+      node.querySelectorAll(".field-radio-group label"),
+      label => label.textContent
+    );
+    expect(labels).eql(["Yes", "No"]);
+  });
+
+  it("should preserve oneOf option ordering for radio widgets", () => {
+    const { node } = createFormComponent({
+      schema: {
+        type: "boolean",
+        oneOf: [
+          {
+            const: false,
+            title: "No",
+          },
+          {
+            const: true,
+            title: "Yes",
+          },
+        ],
+      },
+      formData: true,
+      uiSchema: { "ui:widget": "radio" },
+    });
+
+    const labels = [].map.call(
+      node.querySelectorAll(".field-radio-group label"),
+      label => label.textContent
+    );
+    expect(labels).eql(["No", "Yes"]);
+  });
+
   it("should support inline radio widgets", () => {
     const { node } = createFormComponent({
       schema: { type: "boolean" },
diff --git a/test/Form_test.js b/test/Form_test.js
index 85a39d7f8..793e3a075 100644
--- a/test/Form_test.js
+++ b/test/Form_test.js
@@ -127,6 +127,7 @@ describe("Form", () => {
         ids.push(input.getAttribute("id"));
       }
       expect(ids).to.eql(["rjsf_count"]);
+      expect(node.querySelector("fieldset").id).to.eql("rjsf");
     });
   });
 
@@ -151,6 +152,7 @@ describe("Form", () => {
         ids.push(input.getAttribute("id"));
       }
       expect(ids).to.eql(["rjsf_count"]);
+      expect(node.querySelector("fieldset").id).to.eql("rjsf");
     });
 
     it("should work with oneOf", function() {
diff --git a/test/ObjectField_test.js b/test/ObjectField_test.js
index b546ba55b..2e115813d 100644
--- a/test/ObjectField_test.js
+++ b/test/ObjectField_test.js
@@ -40,7 +40,9 @@ describe("ObjectField", () => {
     it("should render a fieldset", () => {
       const { node } = createFormComponent({ schema });
 
-      expect(node.querySelectorAll("fieldset")).to.have.length.of(1);
+      const fieldset = node.querySelectorAll("fieldset");
+      expect(fieldset).to.have.length.of(1);
+      expect(fieldset[0].id).eql("root");
     });
 
     it("should render a fieldset legend", () => {
diff --git a/test/anyOf_test.js b/test/anyOf_test.js
new file mode 100644
index 000000000..ed5e67475
--- /dev/null
+++ b/test/anyOf_test.js
@@ -0,0 +1,302 @@
+import React from "react";
+import { expect } from "chai";
+import { Simulate } from "react-addons-test-utils";
+
+import { createFormComponent, createSandbox, setProps } from "./test_utils";
+
+describe("anyOf", () => {
+  let sandbox;
+
+  beforeEach(() => {
+    sandbox = createSandbox();
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  it("should not render a select element if the anyOf keyword is not present", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        foo: { type: "string" },
+      },
+    };
+
+    const { node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelectorAll("select")).to.have.length.of(0);
+  });
+
+  it("should render a select element if the anyOf keyword is present", () => {
+    const schema = {
+      type: "object",
+      anyOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelectorAll("select")).to.have.length.of(1);
+  });
+
+  it("should change the rendered form when the select value is changed", () => {
+    const schema = {
+      type: "object",
+      anyOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelectorAll("#root_foo")).to.have.length.of(1);
+    expect(node.querySelectorAll("#root_bar")).to.have.length.of(0);
+
+    const $select = node.querySelector("select");
+
+    Simulate.change($select, {
+      target: { value: $select.options[1].value },
+    });
+
+    expect(node.querySelectorAll("#root_foo")).to.have.length.of(0);
+    expect(node.querySelectorAll("#root_bar")).to.have.length.of(1);
+  });
+
+  it("should handle change events", () => {
+    const schema = {
+      type: "object",
+      anyOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    Simulate.change(node.querySelector("input#root_foo"), {
+      target: { value: "Lorem ipsum dolor sit amet" },
+    });
+
+    expect(comp.state.formData.foo).eql("Lorem ipsum dolor sit amet");
+  });
+
+  it("should clear previous data when changing options", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        buzz: { type: "string" },
+      },
+      anyOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    Simulate.change(node.querySelector("input#root_buzz"), {
+      target: { value: "Lorem ipsum dolor sit amet" },
+    });
+
+    Simulate.change(node.querySelector("input#root_foo"), {
+      target: { value: "Consectetur adipiscing elit" },
+    });
+
+    expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet");
+    expect(comp.state.formData.foo).eql("Consectetur adipiscing elit");
+
+    const $select = node.querySelector("select");
+
+    Simulate.change($select, {
+      target: { value: $select.options[1].value },
+    });
+
+    expect(comp.state.formData.hasOwnProperty("foo")).to.be.false;
+    expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet");
+  });
+
+  it("should support options with different types", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          anyOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    Simulate.change(node.querySelector("input#root_userId"), {
+      target: { value: 12345 },
+    });
+
+    expect(comp.state.formData).eql({
+      userId: 12345,
+    });
+
+    const $select = node.querySelector("select");
+
+    Simulate.change($select, {
+      target: { value: $select.options[1].value },
+    });
+
+    expect(comp.state.formData).eql({
+      userId: undefined,
+    });
+
+    Simulate.change(node.querySelector("input#root_userId"), {
+      target: { value: "Lorem ipsum dolor sit amet" },
+    });
+
+    expect(comp.state.formData).eql({
+      userId: "Lorem ipsum dolor sit amet",
+    });
+  });
+
+  it("should support custom fields", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          anyOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const CustomField = () => {
+      return <div id="custom-anyof-field" />;
+    };
+
+    const { node } = createFormComponent({
+      schema,
+      fields: {
+        AnyOfField: CustomField,
+      },
+    });
+
+    expect(node.querySelectorAll("#custom-anyof-field")).to.have.length(1);
+  });
+
+  it("should select the correct field when the form is rendered from existing data", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          anyOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const { node } = createFormComponent({
+      schema,
+      formData: {
+        userId: "foobarbaz",
+      },
+    });
+
+    expect(node.querySelector("select").value).eql("1");
+  });
+
+  it("should select the correct field when the formData property is updated", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          anyOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelector("select").value).eql("0");
+
+    setProps(comp, {
+      schema,
+      formData: {
+        userId: "foobarbaz",
+      },
+    });
+
+    expect(node.querySelector("select").value).eql("1");
+  });
+});
diff --git a/test/oneOf_test.js b/test/oneOf_test.js
new file mode 100644
index 000000000..25fe39f97
--- /dev/null
+++ b/test/oneOf_test.js
@@ -0,0 +1,302 @@
+import React from "react";
+import { expect } from "chai";
+import { Simulate } from "react-addons-test-utils";
+
+import { createFormComponent, createSandbox, setProps } from "./test_utils";
+
+describe("oneOf", () => {
+  let sandbox;
+
+  beforeEach(() => {
+    sandbox = createSandbox();
+  });
+
+  afterEach(() => {
+    sandbox.restore();
+  });
+
+  it("should not render a select element if the oneOf keyword is not present", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        foo: { type: "string" },
+      },
+    };
+
+    const { node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelectorAll("select")).to.have.length.of(0);
+  });
+
+  it("should render a select element if the oneOf keyword is present", () => {
+    const schema = {
+      type: "object",
+      oneOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelectorAll("select")).to.have.length.of(1);
+  });
+
+  it("should change the rendered form when the select value is changed", () => {
+    const schema = {
+      type: "object",
+      oneOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelectorAll("#root_foo")).to.have.length.of(1);
+    expect(node.querySelectorAll("#root_bar")).to.have.length.of(0);
+
+    const $select = node.querySelector("select");
+
+    Simulate.change($select, {
+      target: { value: $select.options[1].value },
+    });
+
+    expect(node.querySelectorAll("#root_foo")).to.have.length.of(0);
+    expect(node.querySelectorAll("#root_bar")).to.have.length.of(1);
+  });
+
+  it("should handle change events", () => {
+    const schema = {
+      type: "object",
+      oneOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    Simulate.change(node.querySelector("input#root_foo"), {
+      target: { value: "Lorem ipsum dolor sit amet" },
+    });
+
+    expect(comp.state.formData.foo).eql("Lorem ipsum dolor sit amet");
+  });
+
+  it("should clear previous data when changing options", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        buzz: { type: "string" },
+      },
+      oneOf: [
+        {
+          properties: {
+            foo: { type: "string" },
+          },
+        },
+        {
+          properties: {
+            bar: { type: "string" },
+          },
+        },
+      ],
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    Simulate.change(node.querySelector("input#root_buzz"), {
+      target: { value: "Lorem ipsum dolor sit amet" },
+    });
+
+    Simulate.change(node.querySelector("input#root_foo"), {
+      target: { value: "Consectetur adipiscing elit" },
+    });
+
+    expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet");
+    expect(comp.state.formData.foo).eql("Consectetur adipiscing elit");
+
+    const $select = node.querySelector("select");
+
+    Simulate.change($select, {
+      target: { value: $select.options[1].value },
+    });
+
+    expect(comp.state.formData.hasOwnProperty("foo")).to.be.false;
+    expect(comp.state.formData.buzz).eql("Lorem ipsum dolor sit amet");
+  });
+
+  it("should support options with different types", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          oneOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    Simulate.change(node.querySelector("input#root_userId"), {
+      target: { value: 12345 },
+    });
+
+    expect(comp.state.formData).eql({
+      userId: 12345,
+    });
+
+    const $select = node.querySelector("select");
+
+    Simulate.change($select, {
+      target: { value: $select.options[1].value },
+    });
+
+    expect(comp.state.formData).eql({
+      userId: undefined,
+    });
+
+    Simulate.change(node.querySelector("input#root_userId"), {
+      target: { value: "Lorem ipsum dolor sit amet" },
+    });
+
+    expect(comp.state.formData).eql({
+      userId: "Lorem ipsum dolor sit amet",
+    });
+  });
+
+  it("should support custom fields", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          oneOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const CustomField = () => {
+      return <div id="custom-oneof-field" />;
+    };
+
+    const { node } = createFormComponent({
+      schema,
+      fields: {
+        OneOfField: CustomField,
+      },
+    });
+
+    expect(node.querySelectorAll("#custom-oneof-field")).to.have.length(1);
+  });
+
+  it("should select the correct field when the form is rendered from existing data", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          oneOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const { node } = createFormComponent({
+      schema,
+      formData: {
+        userId: "foobarbaz",
+      },
+    });
+
+    expect(node.querySelector("select").value).eql("1");
+  });
+
+  it("should select the correct field when the formData property is updated", () => {
+    const schema = {
+      type: "object",
+      properties: {
+        userId: {
+          oneOf: [
+            {
+              type: "number",
+            },
+            {
+              type: "string",
+            },
+          ],
+        },
+      },
+    };
+
+    const { comp, node } = createFormComponent({
+      schema,
+    });
+
+    expect(node.querySelector("select").value).eql("0");
+
+    setProps(comp, {
+      schema,
+      formData: {
+        userId: "foobarbaz",
+      },
+    });
+
+    expect(node.querySelector("select").value).eql("1");
+  });
+});
diff --git a/test/utils_test.js b/test/utils_test.js
index 0d97f0551..7ba1be760 100644
--- a/test/utils_test.js
+++ b/test/utils_test.js
@@ -292,6 +292,26 @@ describe("utils", () => {
           foo: 42,
         });
       });
+
+      it("should fill array with additional items schema when items is empty", () => {
+        const schema = {
+          type: "object",
+          properties: {
+            array: {
+              type: "array",
+              minItems: 1,
+              additionalItems: {
+                type: "string",
+                default: "foo",
+              },
+              items: [],
+            },
+          },
+        };
+        expect(getDefaultFormState(schema, {})).eql({
+          array: ["foo"],
+        });
+      });
     });
   });
 
diff --git a/test/validate_test.js b/test/validate_test.js
index 0b352b74b..cb437e894 100644
--- a/test/validate_test.js
+++ b/test/validate_test.js
@@ -3,10 +3,40 @@ import { expect } from "chai";
 import sinon from "sinon";
 import { Simulate } from "react-addons-test-utils";
 
-import validateFormData, { toErrorList } from "../src/validate";
+import validateFormData, { isValid, toErrorList } from "../src/validate";
 import { createFormComponent } from "./test_utils";
 
 describe("Validation", () => {
+  describe("validate.isValid()", () => {
+    it("should return true if the data is valid against the schema", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: { type: "string" },
+        },
+      };
+
+      expect(isValid(schema, { foo: "bar" })).to.be.true;
+    });
+
+    it("should return false if the data is not valid against the schema", () => {
+      const schema = {
+        type: "object",
+        properties: {
+          foo: { type: "string" },
+        },
+      };
+
+      expect(isValid(schema, { foo: 12345 })).to.be.false;
+    });
+
+    it("should return false if the schema is invalid", () => {
+      const schema = "foobarbaz";
+
+      expect(isValid(schema, { foo: "bar" })).to.be.false;
+    });
+  });
+
   describe("validate.validateFormData()", () => {
     describe("No custom validate function", () => {
       const illFormedKey = "bar.'\"[]()=+*&^%$#@!";