Repository: CSCfi/rems
Pull request #1399 (2019-07-12 12:15:36)

Form schema unification
On top of #1398. Part of #913. Unifies the read and write models for forms.


Patch:
diff --git a/resources/migrations/20190711111351-refactor-form-templates.edn b/resources/migrations/20190711111351-refactor-form-templates.edn
new file mode 100644
index 000000000..eb05ca595
--- /dev/null
+++ b/resources/migrations/20190711111351-refactor-form-templates.edn
@@ -0,0 +1,3 @@
+{:ns rems.migrations.refactor-form-templates
+ :up-fn migrate-up
+ :down-fn nil}
diff --git a/resources/translations/en.edn b/resources/translations/en.edn
index 1531eceb3..0d3dff16b 100644
--- a/resources/translations/en.edn
+++ b/resources/translations/en.edn
@@ -168,12 +168,12 @@
                 :add-option "Add option"
                 :field-n "Field %1"
                 :field-title "Field title"
-                :input-prompt "Placeholder text (optional)"
                 :maxlength "Maximum length (optional)"
                 :option-key "ID code"
                 :option-label "Label"
                 :option-n "Option %1"
                 :optional "Optional"
+                :placeholder "Placeholder text (optional)"
                 :title "Form name"
                 :type-attachment "Attachment"
                 :type-date "Date field"
diff --git a/resources/translations/fi.edn b/resources/translations/fi.edn
index 1b4fd772f..b81289969 100644
--- a/resources/translations/fi.edn
+++ b/resources/translations/fi.edn
@@ -168,12 +168,12 @@
                 :add-option "Lisää vaihtoehto"
                 :field-n "Kenttä %1"
                 :field-title "Kentän otsikko"
-                :input-prompt "Esimerkkiteksti (ei pakollinen)"
                 :maxlength "Enimmäispituus (ei pakollinen)"
                 :option-key "Tunniste"
                 :option-label "Otsikko"
                 :option-n "Vaihtoehto %1"
                 :optional "Ei pakollinen"
+                :placeholder "Esimerkkiteksti (ei pakollinen)"
                 :title "Lomakkeen nimi"
                 :type-attachment "Liitetiedosto"
                 :type-date "Päivämäärä"
diff --git a/src/clj/rems/api/forms.clj b/src/clj/rems/api/forms.clj
index 9161c57b3..2ba99f54c 100644
--- a/src/clj/rems/api/forms.clj
+++ b/src/clj/rems/api/forms.clj
@@ -1,69 +1,25 @@
 (ns rems.api.forms
   (:require [compojure.api.sweet :refer :all]
-            [rems.api.schema :refer [SuccessResponse UpdateStateCommand]]
-            [rems.api.util]
+            [rems.api.schema :refer [SuccessResponse UpdateStateCommand FormTemplateOverview NewFieldTemplate FormTemplate]]
+            [rems.api.util :refer [not-found-json-response]]
             [rems.db.form :as form]
             [rems.util :refer [getx-user-id]]
             [ring.util.http-response :refer :all]
-            [schema.core :as s])
-  (:import (org.joda.time DateTime)))
-
-(s/defschema Form
-  {:id s/Num
-   :organization s/Str
-   :title s/Str
-   :start DateTime
-   :end (s/maybe DateTime)
-   :expired s/Bool
-   :enabled s/Bool
-   :archived s/Bool})
-
-(def not-neg? (partial <= 0))
-
-(s/defschema FormField
-  {:title {s/Keyword s/Str}
-   :optional s/Bool
-   :type (s/enum "attachment" "date" "description" "label" "multiselect" "option" "text" "texta")
-   (s/optional-key :maxlength) (s/maybe (s/constrained s/Int not-neg?))
-   (s/optional-key :options) [{:key s/Str
-                               :label {s/Keyword s/Str}}]
-   (s/optional-key :input-prompt) {s/Keyword s/Str}})
-
-(s/defschema FormFieldWithId
-  (merge FormField
-         {:id s/Int}))
-
-(s/defschema FullForm
-  (merge Form
-         {:fields [FormFieldWithId]}))
-
-(s/defschema Forms
-  [Form])
-
-(defn- format-form
-  [{:keys [id organization title start end expired enabled archived]}]
-  {:id id
-   :organization organization
-   :title title
-   :start start
-   :end end
-   :expired expired
-   :enabled enabled
-   :archived archived})
+            [schema.core :as s]))
 
 (defn- get-form-templates [filters]
   (doall
-   (for [wf (form/get-form-templates filters)]
-     (format-form wf))))
+   (for [form (form/get-form-templates filters)]
+     (select-keys form [:form/id :form/organization :form/title :start :end :expired :enabled :archived]))))
 
-(s/defschema FormCommand
-  {:organization s/Str
-   :title s/Str
-   :fields [FormField]})
+(s/defschema CreateFormCommand
+  {:form/organization s/Str
+   :form/title s/Str
+   :form/fields [NewFieldTemplate]})
 
-(s/defschema FormResponse
+(s/defschema CreateFormResponse
   {:success s/Bool
-   :id s/Num})
+   :id s/Int})
 
 (def forms-api
   (context "/forms" []
@@ -75,7 +31,7 @@
       :query-params [{disabled :- (describe s/Bool "whether to include disabled forms") false}
                      {expired :- (describe s/Bool "whether to include expired forms") false}
                      {archived :- (describe s/Bool "whether to include archived forms") false}]
-      :return Forms
+      :return [FormTemplateOverview]
       (ok (get-form-templates (merge (when-not expired {:expired false})
                                      (when-not disabled {:enabled true})
                                      (when-not archived {:archived false})))))
@@ -83,16 +39,19 @@
     (POST "/create" []
       :summary "Create form"
       :roles #{:owner}
-      :body [command FormCommand]
-      :return FormResponse
+      :body [command CreateFormCommand]
+      :return CreateFormResponse
       (ok (form/create-form! (getx-user-id) command)))
 
     (GET "/:form-id" []
       :summary "Get form by id"
       :roles #{:owner}
-      :path-params [form-id :- (describe s/Num "form-id")]
-      :return FullForm
-      (ok (form/get-form-template form-id)))
+      :path-params [form-id :- (describe s/Int "form-id")]
+      :return FormTemplate
+      (let [form (form/get-form-template form-id)]
+        (if form
+          (ok form)
+          (not-found-json-response))))
 
     (GET "/:form-id/editable" []
       :summary "Check if the form is editable"
@@ -106,7 +65,7 @@
       :summary "Edit form"
       :roles #{:owner}
       :path-params [form-id :- (describe s/Num "form-id")]
-      :body [command FormCommand]
+      :body [command CreateFormCommand]
       :return SuccessResponse
       (ok (form/edit-form! (getx-user-id) form-id command)))
 
diff --git a/src/clj/rems/api/schema.clj b/src/clj/rems/api/schema.clj
index 9f616f40d..d80547650 100644
--- a/src/clj/rems/api/schema.clj
+++ b/src/clj/rems/api/schema.clj
@@ -108,17 +108,79 @@
    :license/expired s/Bool
    :license/archived s/Bool})
 
-(s/defschema Field
+(def UserId s/Str)
+
+(s/defschema Actor
+  {:actoruserid UserId
+   :round s/Num
+   :role (s/enum "approver" "reviewer")})
+
+(s/defschema WorkflowLicense
+  {:type s/Str
+   :start DateTime
+   :textcontent s/Str
+   :localizations [s/Any]
+   :end (s/maybe DateTime)})
+
+(s/defschema WorkflowDB ; TODO: unify workflow schemas
+  {:id s/Num
+   :organization s/Str
+   :owneruserid UserId
+   :modifieruserid UserId
+   :title s/Str
+   :fnlround s/Num
+   :workflow s/Any
+   :licenses s/Any
+   :visibility s/Str
+   :start DateTime
+   :end (s/maybe DateTime)
+   :expired s/Bool
+   :enabled s/Bool
+   :archived s/Bool})
+
+(s/defschema Workflow
+  (-> WorkflowDB
+      (dissoc :fnlround
+              :licenses
+              :visibility)
+      (assoc :final-round s/Num
+             :actors [Actor]
+             :licenses [WorkflowLicense])))
+
+(def not-neg? (partial <= 0))
+
+(s/defschema FieldTemplate
   {:field/id s/Int
-   :field/value s/Str
-   (s/optional-key :field/previous-value) s/Str
    :field/type (s/enum :attachment :date :description :label :multiselect :option :text :texta)
    :field/title LocalizedString
-   :field/placeholder LocalizedString
+   (s/optional-key :field/placeholder) LocalizedString
    :field/optional s/Bool
-   :field/options [{:key s/Str
-                    :label LocalizedString}]
-   :field/max-length (s/maybe s/Int)})
+   (s/optional-key :field/options) [{:key s/Str
+                                     :label LocalizedString}]
+   (s/optional-key :field/max-length) (s/maybe (s/constrained s/Int not-neg?))})
+
+(s/defschema NewFieldTemplate
+  (dissoc FieldTemplate :field/id))
+
+(s/defschema Field
+  (assoc FieldTemplate
+         :field/value s/Str
+         (s/optional-key :field/previous-value) s/Str))
+
+(s/defschema FormTemplate
+  {:form/id s/Int
+   :form/organization s/Str
+   :form/title s/Str
+   :form/fields [FieldTemplate]
+   ;; TODO: rename the following to use :status/ namespace (also in all other entities)
+   :start DateTime
+   :end (s/maybe DateTime)
+   :expired s/Bool
+   :enabled s/Bool
+   :archived s/Bool})
+
+(s/defschema FormTemplateOverview
+  (dissoc FormTemplate :form/fields))
 
 (s/defschema Form
   {:form/id s/Int
diff --git a/src/clj/rems/api/workflows.clj b/src/clj/rems/api/workflows.clj
index a6d333d3e..3cc86a870 100644
--- a/src/clj/rems/api/workflows.clj
+++ b/src/clj/rems/api/workflows.clj
@@ -3,7 +3,7 @@
             [clojure.test :refer [deftest is]]
             [compojure.api.sweet :refer :all]
             [rems.api.applications :refer [User get-users format-user]]
-            [rems.api.schema :refer [SuccessResponse]]
+            [rems.api.schema :refer [SuccessResponse UserId Workflow]]
             [rems.api.util]
             [rems.db.core :as db]
             [rems.db.users :as users]
@@ -13,20 +13,6 @@
             [schema.core :as s])
   (:import [org.joda.time DateTime DateTimeZone]))
 
-(def UserId s/Str)
-
-(s/defschema Actor
-  {:actoruserid UserId
-   :round s/Num
-   :role (s/enum "approver" "reviewer")})
-
-(s/defschema WorkflowLicense
-  {:type s/Str
-   :start DateTime
-   :textcontent s/Str
-   :localizations [s/Any]
-   :end (s/maybe DateTime)})
-
 (defn parse-db-time [s]
   (when s
     (-> (DateTime/parse s)
@@ -43,25 +29,6 @@
       (assoc :start (parse-db-time (:start license)))
       (assoc :end (parse-db-time (:end license)))))
 
-(s/defschema Workflow
-  {:id s/Num
-   :organization s/Str
-   :owneruserid UserId
-   :modifieruserid UserId
-   :title s/Str
-   :final-round s/Num
-   :workflow s/Any
-   :start DateTime
-   :end (s/maybe DateTime)
-   :expired s/Bool
-   :enabled s/Bool
-   :archived s/Bool
-   :actors [Actor]
-   :licenses [WorkflowLicense]})
-
-(s/defschema Workflows
-  [Workflow])
-
 (defn- format-workflow
   [{:keys [id organization owneruserid modifieruserid title fnlround workflow start end expired enabled archived licenses actors]}]
   {:id id
@@ -129,7 +96,7 @@
       :query-params [{disabled :- (describe s/Bool "whether to include disabled workflows") false}
                      {expired :- (describe s/Bool "whether to include expired workflows") false}
                      {archived :- (describe s/Bool "whether to include archived workflows") false}]
-      :return Workflows
+      :return [Workflow]
       (ok (get-workflows (merge (when-not expired {:expired false})
                                 (when-not disabled {:enabled true})
                                 (when-not archived {:archived false})))))
diff --git a/src/clj/rems/application/model.clj b/src/clj/rems/application/model.clj
index 4fdcbe762..26017b896 100644
--- a/src/clj/rems/application/model.clj
+++ b/src/clj/rems/application/model.clj
@@ -420,22 +420,15 @@
          (localization-for :title {:localizations {:en {}
                                                    :fi {}}}))))
 
-(defn- enrich-form [app-form get-form-template]
-  (let [form (get-form-template (:form/id app-form))
-        app-fields (:form/fields app-form)
-        rich-fields (map (fn [field]
-                           {:field/id (:id field)
-                            :field/value "" ; default for new forms
-                            :field/type (keyword (:type field))
-                            :field/title (:title field)
-                            :field/placeholder (get field :input-prompt {})
-                            :field/optional (:optional field)
-                            :field/options (:options field)
-                            :field/max-length (:maxlength field)})
-                         (:fields form))
-        fields (merge-lists-by :field/id rich-fields app-fields)]
-    (assoc app-form
-           :form/title (:title form)
+(defn- enrich-form [form get-form-template]
+  (let [form-template (get-form-template (:form/id form))
+        default-fields (map #(assoc % :field/value "")
+                            (:form/fields form-template))
+        fields (merge-lists-by :field/id
+                               default-fields
+                               (:form/fields form))]
+    (assoc form
+           :form/title (:form/title form-template)
            :form/fields fields)))
 
 (defn- set-application-description [application]
diff --git a/src/clj/rems/db/form.clj b/src/clj/rems/db/form.clj
index 78397584f..226321711 100644
--- a/src/clj/rems/db/form.clj
+++ b/src/clj/rems/db/form.clj
@@ -1,15 +1,35 @@
 (ns rems.db.form
   (:require [clojure.test :refer :all]
+            [medley.core :refer [map-keys]]
+            [rems.api.schema :refer [FieldTemplate]]
             [rems.db.catalogue :as catalogue]
             [rems.db.core :as db]
-            [rems.json :as json]))
+            [rems.json :as json]
+            [schema.coerce :as coerce]
+            [schema.core :as s]))
 
-;;; form api related code – form "templates"
+(def ^:private fields-coercer
+  (coerce/coercer [FieldTemplate] coerce/string-coercion-matcher))
+
+(defn- coerce-fields [fields]
+  (let [result (fields-coercer fields)]
+    (if (schema.utils/error? result)
+      (throw (ex-info "Failed to coerce fields" {:fields fields :error result}))
+      result)))
 
 (defn- parse-db-row [row]
   (-> row
-      (update :fields json/parse-string)
-      db/assoc-expired))
+      (update :fields #(coerce-fields (json/parse-string %)))
+      db/assoc-expired
+      (->> (map-keys {:id :form/id
+                      :organization :form/organization
+                      :title :form/title
+                      :fields :form/fields
+                      :start :start
+                      :end :end
+                      :expired :expired
+                      :enabled :enabled
+                      :archived :archived}))))
 
 (defn get-form-templates [filters]
   (->> (db/get-form-templates)
@@ -17,11 +37,12 @@
        (db/apply-filters filters)))
 
 (defn get-form-template [id]
-  (-> (db/get-form-template {:id id})
-      parse-db-row))
+  (let [row (db/get-form-template {:id id})]
+    (when row
+      (parse-db-row row))))
 
-(defn- create-form-item! [user-id form-id item-index {:keys [title optional type input-prompt maxlength options]}]
-  (let [item-id (:id (db/create-form-item! {:type type
+(defn- create-form-item! [user-id form-id item-index {:field/keys [title optional type max-length options placeholder]}]
+  (let [item-id (:id (db/create-form-item! {:type (name type)
                                             :user user-id
                                             :value 0}))]
     (doseq [[index option] (map-indexed vector options)]
@@ -34,14 +55,14 @@
     (db/link-form-item! {:form form-id
                          :itemorder item-index
                          :optional optional
-                         :maxlength maxlength
+                         :maxlength max-length
                          :item item-id
                          :user user-id})
     (doseq [lang (keys title)]
       (db/localize-form-item! {:item item-id
                                :langcode (name lang)
                                :title (get title lang)
-                               :inputprompt (get input-prompt lang)}))
+                               :inputprompt (get placeholder lang)}))
     item-id))
 
 (defn- catalogue-items-for-form [id]
@@ -65,34 +86,37 @@
   ;; order, or generate UUIDs)
   (map-indexed (fn [order field]
                  (let [id (create-form-item! user-id form-id order field)]
-                   (assoc field :id id)))
+                   (assoc field :field/id id)))
                fields))
 
-(defn create-form! [user-id {:keys [organization title fields] :as form}]
+(defn create-form! [user-id form]
   ;; FIXME Remove saving old style forms only when we have a db migration.
   ;;       Otherwise it will get reeealy tricky to return both versions in get-api.
   (let [;; NB: Legacy forms (created by db/create-form!) are not updated
         ;;   when the form is edited. This is on purpose: this whole legacy
         ;;   codepath will be removed soon.
-        form-id (:id (db/create-form! {:organization organization
-                                       :title title
-                                       :user user-id}))
-        fields-with-ids (generate-fields-with-ids! user-id form-id fields)]
-    (db/save-form-template!
-     (assoc form
-            :id form-id
-            :user user-id
-            :fields (json/generate-string fields-with-ids)))
+        form-id (:id (db/create-form! {:organization (:form/organization form)
+                                       :title (:form/title form)
+                                       :user user-id}))]
+    (db/save-form-template! {:id form-id
+                             :organization (:form/organization form)
+                             :title (:form/title form)
+                             :user user-id
+                             :fields (->> (generate-fields-with-ids! user-id form-id (:form/fields form))
+                                          (s/validate [FieldTemplate])
+                                          (json/generate-string))})
     {:success (not (nil? form-id))
      :id form-id}))
 
-(defn edit-form! [user-id form-id {:keys [fields] :as form}]
+(defn edit-form! [user-id form-id form]
   (or (form-in-use-error form-id)
-      (do (db/edit-form-template!
-           (assoc form
-                  :id form-id
-                  :user user-id
-                  :fields (json/generate-string (generate-fields-with-ids! user-id form-id fields))))
+      (do (db/edit-form-template! {:id form-id
+                                   :organization (:form/organization form)
+                                   :title (:form/title form)
+                                   :user user-id
+                                   :fields (->> (generate-fields-with-ids! user-id form-id (:form/fields form))
+                                                (s/validate [FieldTemplate])
+                                                (json/generate-string))})
           {:success true})))
 
 (defn update-form! [command]
diff --git a/src/clj/rems/migrations/refactor_form_templates.clj b/src/clj/rems/migrations/refactor_form_templates.clj
new file mode 100644
index 000000000..32644656b
--- /dev/null
+++ b/src/clj/rems/migrations/refactor_form_templates.clj
@@ -0,0 +1,36 @@
+(ns rems.migrations.refactor-form-templates
+  (:require [hugsql.core :as hugsql]
+            [medley.core :refer [map-keys]]
+            [rems.json :as json]))
+
+(hugsql/def-db-fns-from-string
+ "
+-- :name get-form-templates :? :*
+SELECT
+ id,
+ fields::TEXT
+FROM form_template;
+
+-- :name update-form-template! :!
+UPDATE form_template
+SET fields = :fields::jsonb
+WHERE id = :id;
+")
+
+(defn- migrate-field [field]
+  (map-keys {:id :field/id
+             :type :field/type
+             :title :field/title
+             :input-prompt :field/placeholder
+             :optional :field/optional
+             :options :field/options
+             :maxlength :field/max-length}
+            field))
+
+(defn migrate-up [{:keys [conn]}]
+  (doseq [{:keys [id fields]} (get-form-templates conn)]
+    (update-form-template! conn {:id id
+                                 :fields (->> fields
+                                              json/parse-string
+                                              (map migrate-field)
+                                              json/generate-string)})))
diff --git a/src/clj/rems/validate.clj b/src/clj/rems/validate.clj
index 1cdc0a8da..4ec221694 100644
--- a/src/clj/rems/validate.clj
+++ b/src/clj/rems/validate.clj
@@ -9,7 +9,7 @@
 
 (defn validate-forms []
   (doseq [template (form/get-form-templates {})]
-    (s/validate rems.api.forms/FullForm template)))
+    (s/validate rems.api.schema/FormTemplate template)))
 
 (defn validate []
   (log/info "Validating data")
diff --git a/src/cljs/rems/administration/create_catalogue_item.cljs b/src/cljs/rems/administration/create_catalogue_item.cljs
index 90ec4fcbf..8abf74663 100644
--- a/src/cljs/rems/administration/create_catalogue_item.cljs
+++ b/src/cljs/rems/administration/create_catalogue_item.cljs
@@ -173,11 +173,11 @@
      [autocomplete/component
       {:value (when selected-form #{selected-form})
        :items forms
-       :value->text #(:title %2)
-       :item->key :id
-       :item->text :title
+       :value->text #(:form/title %2)
+       :item->key :form/id
+       :item->text :form/title
        :item->value identity
-       :search-fields [:title]
+       :search-fields [:form/title]
        :add-fn #(rf/dispatch [::set-selected-form %])
        :remove-fn #(rf/dispatch [::set-selected-form nil])}]]))
 
diff --git a/src/cljs/rems/administration/create_form.cljs b/src/cljs/rems/administration/create_form.cljs
index 4a927e5d2..1bb8d9704 100644
--- a/src/cljs/rems/administration/create_form.cljs
+++ b/src/cljs/rems/administration/create_form.cljs
@@ -18,7 +18,7 @@
  ::enter-page
  (fn [{:keys [db]} [_ form-id edit-form?]]
    {:db (assoc db
-               ::form {:fields []}
+               ::form {:form/fields []}
                ::form-id form-id
                ::edit-form? edit-form?)
     :dispatch-n [[::fetch-form form-id]]}))
@@ -47,68 +47,68 @@
 (rf/reg-sub ::edit-form? (fn [db _] (::edit-form? db)))
 (rf/reg-event-db ::set-form-field (fn [db [_ keys value]] (assoc-in db (concat [::form] keys) value)))
 
-(rf/reg-event-db ::add-form-field (fn [db [_]] (update-in db [::form :fields] items/add {:type "text"})))
-(rf/reg-event-db ::remove-form-field (fn [db [_ field-index]] (update-in db [::form :fields] items/remove field-index)))
-(rf/reg-event-db ::move-form-field-up (fn [db [_ field-index]] (update-in db [::form :fields] items/move-up field-index)))
-(rf/reg-event-db ::move-form-field-down (fn [db [_ field-index]] (update-in db [::form :fields] items/move-down field-index)))
+(rf/reg-event-db ::add-form-field (fn [db [_]] (update-in db [::form :form/fields] items/add {:field/type :text})))
+(rf/reg-event-db ::remove-form-field (fn [db [_ field-index]] (update-in db [::form :form/fields] items/remove field-index)))
+(rf/reg-event-db ::move-form-field-up (fn [db [_ field-index]] (update-in db [::form :form/fields] items/move-up field-index)))
+(rf/reg-event-db ::move-form-field-down (fn [db [_ field-index]] (update-in db [::form :form/fields] items/move-down field-index)))
 
 (rf/reg-event-db
  ::add-form-field-option
  (fn [db [_ field-index]]
-   (update-in db [::form :fields field-index :options] items/add {})))
+   (update-in db [::form :form/fields field-index :field/options] items/add {})))
 
 (rf/reg-event-db
  ::remove-form-field-option
  (fn [db [_ field-index option-index]]
-   (update-in db [::form :fields field-index :options] items/remove option-index)))
+   (update-in db [::form :form/fields field-index :field/options] items/remove option-index)))
 
 (rf/reg-event-db
  ::move-form-field-option-up
  (fn [db [_ field-index option-index]]
-   (update-in db [::form :fields field-index :options] items/move-up option-index)))
+   (update-in db [::form :form/fields field-index :field/options] items/move-up option-index)))
 
 (rf/reg-event-db
  ::move-form-field-option-down
  (fn [db [_ field-index option-index]]
-   (update-in db [::form :fields field-index :options] items/move-down option-index)))
+   (update-in db [::form :form/fields field-index :field/options] items/move-down option-index)))
 
 ;;;; form submit
 
 (defn- supports-optional? [field]
-  (not= "label" (:type field)))
+  (not= :label (:field/type field)))
 
-(defn- supports-input-prompt? [field]
-  (contains? #{"text" "texta" "description"} (:type field)))
+(defn- supports-placeholder? [field]
+  (contains? #{:text :texta :description} (:field/type field)))
 
-(defn- supports-maxlength? [field]
-  (contains? #{"text" "texta"} (:type field)))
+(defn- supports-max-length? [field]
+  (contains? #{:text :texta} (:field/type field)))
 
 (defn- supports-options? [field]
-  (contains? #{"option" "multiselect"} (:type field)))
+  (contains? #{:option :multiselect} (:field/type field)))
 
 (defn build-localized-string [lstr languages]
   (into {} (for [language languages]
              [language (get lstr language "")])))
 
 (defn- build-request-field [field languages]
-  (merge {:title (build-localized-string (:title field) languages)
-          :type (:type field)
-          :optional (if (supports-optional? field)
-                      (boolean (:optional field))
-                      false)}
-         (when (supports-input-prompt? field)
-           {:input-prompt (build-localized-string (:input-prompt field) languages)})
-         (when (supports-maxlength? field)
-           {:maxlength (parse-int (:maxlength field))})
+  (merge {:field/title (build-localized-string (:field/title field) languages)
+          :field/type (:field/type field)
+          :field/optional (if (supports-optional? field)
+                            (boolean (:field/optional field))
+                            false)}
+         (when (supports-placeholder? field)
+           {:field/placeholder (build-localized-string (:field/placeholder field) languages)})
+         (when (supports-max-length? field)
+           {:field/max-length (parse-int (:field/max-length field))})
          (when (supports-options? field)
-           {:options (for [{:keys [key label]} (:options field)]
-                       {:key key
-                        :label (build-localized-string label languages)})})))
+           {:field/options (for [{:keys [key label]} (:field/options field)]
+                             {:key key
+                              :label (build-localized-string label languages)})})))
 
 (defn build-request [form languages]
-  {:organization (:organization form)
-   :title (:title form)
-   :fields (mapv #(build-request-field % languages) (:fields form))})
+  {:form/organization (:form/organization form)
+   :form/title (:form/title form)
+   :form/fields (mapv #(build-request-field % languages) (:form/fields form))})
 
 ;;;; form validation
 
@@ -125,33 +125,33 @@
     (when (not-empty (remove identity validated))
       {key (apply merge validated)})))
 
-(def ^:private maxlength-range [0 32767])
+(def ^:private max-length-range [0 32767])
 
-(defn- validate-maxlength [maxlength]
-  (when-not (str/blank? maxlength)
-    (let [parsed (parse-int maxlength)]
+(defn- validate-max-length [max-length]
+  (when-not (str/blank? max-length)
+    (let [parsed (parse-int max-length)]
       (when (or (nil? parsed)
-                (not (<= (first maxlength-range) parsed (second maxlength-range))))
-        {:maxlength :t.form.validation/invalid-value}))))
+                (not (<= (first max-length-range) parsed (second max-length-range))))
+        {:field/max-length :t.form.validation/invalid-value}))))
 
 (defn- validate-option [option id languages]
   {id (merge (validate-text-field option :key)
              (validate-localized-text-field option :label languages))})
 
 (defn- validate-options [options languages]
-  {:options (apply merge (mapv #(validate-option %1 %2 languages) options (range)))})
+  {:field/options (apply merge (mapv #(validate-option %1 %2 languages) options (range)))})
 
 (defn- validate-field [field id languages]
-  {id (merge (validate-text-field field :type)
-             (validate-localized-text-field field :title languages)
-             (validate-optional-localized-field field :input-prompt languages)
-             (validate-maxlength (:maxlength field))
-             (validate-options (:options field) languages))})
+  {id (merge (validate-text-field field :field/type)
+             (validate-localized-text-field field :field/title languages)
+             (validate-optional-localized-field field :field/placeholder languages)
+             (validate-max-length (:field/max-length field))
+             (validate-options (:field/options field) languages))})
 
 (defn validate-form [form languages]
-  (-> (merge (validate-text-field form :organization)
-             (validate-text-field form :title)
-             {:fields (apply merge (mapv #(validate-field %1 %2 languages) (form :fields) (range)))})
+  (-> (merge (validate-text-field form :form/organization)
+             (validate-text-field form :form/title)
+             {:form/fields (apply merge (mapv #(validate-field %1 %2 languages) (:form/fields form) (range)))})
       remove-empty-keys))
 
 (defn- page-title [edit-form?]
@@ -174,7 +174,7 @@
                   {:params (build-request (db ::form) (db :languages))
                    :handler (partial status-modal/common-success-handler! #(dispatch! (str "#/administration/forms/" (or (db ::form-id) (:id %)))))
                    :error-handler status-modal/common-error-handler!})
-      {:db (assoc db ::form-errors form-errors)}))))
+       {:db (assoc db ::form-errors form-errors)}))))
 
 ;;;; UI
 
@@ -184,24 +184,24 @@
    :update-form ::set-form-field})
 
 (defn- form-organization-field []
-  [text-field context {:keys [:organization]
+  [text-field context {:keys [:form/organization]
                        :label (text :t.administration/organization)
                        :placeholder (text :t.administration/organization-placeholder)}])
 
 (defn- form-title-field []
-  [text-field context {:keys [:title]
+  [text-field context {:keys [:form/title]
                        :label (text :t.create-form/title)}])
 
 (defn- form-field-title-field [field-index]
-  [localized-text-field context {:keys [:fields field-index :title]
+  [localized-text-field context {:keys [:form/fields field-index :field/title]
                                  :label (text :t.create-form/field-title)}])
 
-(defn- form-field-input-prompt-field [field-index]
-  [localized-text-field context {:keys [:fields field-index :input-prompt]
-                                 :label (text :t.create-form/input-prompt)}])
+(defn- form-field-placeholder-field [field-index]
+  [localized-text-field context {:keys [:form/fields field-index :field/placeholder]
+                                 :label (text :t.create-form/placeholder)}])
 
-(defn- form-field-maxlength-field [field-index]
-  [text-field context {:keys [:fields field-index :maxlength]
+(defn- form-field-max-length-field [field-index]
+  [text-field context {:keys [:form/fields field-index :field/max-length]
                        :label (text :t.create-form/maxlength)}])
 
 (defn- add-form-field-option-button [field-index]
@@ -228,35 +228,35 @@
      [move-form-field-option-up-button field-index option-index]
      [move-form-field-option-down-button field-index option-index]
      [remove-form-field-option-button field-index option-index]]]
-   [text-field context {:keys [:fields field-index :options option-index :key]
+   [text-field context {:keys [:form/fields field-index :field/options option-index :key]
                         :label (text :t.create-form/option-key)
                         :normalizer normalize-option-key}]
-   [localized-text-field context {:keys [:fields field-index :options option-index :label]
+   [localized-text-field context {:keys [:form/fields field-index :field/options option-index :label]
                                   :label (text :t.create-form/option-label)}]])
 
 (defn- form-field-option-fields [field-index]
   (let [form @(rf/subscribe [::form])]
     (into (into [:div]
-                (for [option-index (range (count (get-in form [:fields field-index :options])))]
+                (for [option-index (range (count (get-in form [:form/fields field-index :field/options])))]
                   [form-field-option-field field-index option-index]))
           [[:div.form-field-option.new-form-field-option
             [add-form-field-option-button field-index]]])))
 
 (defn- form-field-type-radio-group [field-index]
   [radio-button-group context {:id (str "radio-group-" field-index)
-                               :keys [:fields field-index :type]
+                               :keys [:form/fields field-index :field/type]
                                :orientation :vertical
-                               :options [{:value "text", :label (text :t.create-form/type-text)}
-                                         {:value "texta", :label (text :t.create-form/type-texta)}
-                                         {:value "description", :label (text :t.create-form/type-description)}
-                                         {:value "option", :label (text :t.create-form/type-option)}
-                                         {:value "multiselect", :label (text :t.create-form/type-multiselect)}
-                                         {:value "date", :label (text :t.create-form/type-date)}
-                                         {:value "attachment", :label (text :t.create-form/type-attachment)}
-                                         {:value "label", :label (text :t.create-form/type-label)}]}])
+                               :options [{:value :text, :label (text :t.create-form/type-text)}
+                                         {:value :texta, :label (text :t.create-form/type-texta)}
+                                         {:value :description, :label (text :t.create-form/type-description)}
+                                         {:value :option, :label (text :t.create-form/type-option)}
+                                         {:value :multiselect, :label (text :t.create-form/type-multiselect)}
+                                         {:value :date, :label (text :t.create-form/type-date)}
+                                         {:value :attachment, :label (text :t.create-form/type-attachment)}
+                                         {:value :label, :label (text :t.create-form/type-label)}]}])
 
 (defn- form-field-optional-checkbox [field-index]
-  [checkbox context {:keys [:fields field-index :optional]
+  [checkbox context {:keys [:form/fields field-index :field/optional]
                      :label (text :t.create-form/optional)}])
 
 (defn- add-form-field-button []
@@ -302,38 +302,21 @@
                         [form-field-type-radio-group field-index]
                         (when (supports-optional? field)
                           [form-field-optional-checkbox field-index])
-                        (when (supports-input-prompt? field)
-                          [form-field-input-prompt-field field-index])
-                        (when (supports-maxlength? field)
-                          [form-field-maxlength-field field-index])
+                        (when (supports-placeholder? field)
+                          [form-field-placeholder-field field-index])
+                        (when (supports-max-length? field)
+                          [form-field-max-length-field field-index])
                         (when (supports-options? field)
                           [form-field-option-fields field-index])])
                      fields)))
 
-(defn- form-field-to-application-field
-  "Convert a field from the form create model to the application view model."
-  [field]
-  (merge {:field/type (keyword (:type field))
-          :field/title (:title field)}
-         (when (supports-optional? field)
-           {:field/optional (:optional field)})
-         (when (supports-input-prompt? field)
-           {:field/placeholder (:input-prompt field)})
-         (when (supports-maxlength? field)
-           {:field/max-length (parse-int (:maxlength field))})
-         (when (supports-options? field)
-           {:field/options (:options field)})))
-
-(defn- field-preview [field]
-  [fields/field (form-field-to-application-field field)])
-
 (defn form-preview [form]
   [collapsible/component
    {:id "preview-form"
     :title (text :t.administration/preview)
     :always (into [:div]
-                  (for [field (:fields form)]
-                    [field-preview field]))}])
+                  (for [field (:form/fields form)]
+                    [fields/field field]))}])
 
 (defn create-form-page []
   (let [form @(rf/subscribe [::form])
@@ -353,7 +336,7 @@
             :always [:div
                      [form-organization-field]
                      [form-title-field]
-                     [form-fields (:fields form)]
+                     [form-fields (:form/fields form)]
 
                      [:div.form-field.new-form-field
                       [add-form-field-button]]
diff --git a/src/cljs/rems/administration/form.cljs b/src/cljs/rems/administration/form.cljs
index f044dda8d..601c3c779 100644
--- a/src/cljs/rems/administration/form.cljs
+++ b/src/cljs/rems/administration/form.cljs
@@ -63,27 +63,26 @@
     :on-click #(dispatch! (str "/#/administration/create-form/" id))}
    (text :t.administration/copy-as-new)])
 
-(defn form-view [form language]
+(defn form-view [form]
   [:div.spaced-vertically-3
    [collapsible/component
     {:id "form"
-     :title [:span (andstr (:organization form) "/") (:title form)]
+     :title [:span (andstr (:form/organization form) "/") (:form/title form)]
      :always [:div
-              [inline-info-field (text :t.administration/organization) (:organization form)]
-              [inline-info-field (text :t.administration/title) (:title form)]
+              [inline-info-field (text :t.administration/organization) (:form/organization form)]
+              [inline-info-field (text :t.administration/title) (:form/title form)]
               [inline-info-field (text :t.administration/start) (localize-time (:start form))]
               [inline-info-field (text :t.administration/end) (localize-time (:end form))]
               [inline-info-field (text :t.administration/active) [readonly-checkbox (not (:expired form))]]]}]
    [:div.col.commands
     [back-button]
-    [edit-button (:id form)]
-    [copy-as-new-button (:id form)]]
+    [edit-button (:form/id form)]
+    [copy-as-new-button (:form/id form)]]
    [form-preview form]])
 ;; TODO Do we support form licenses?
 
 (defn form-page []
   (let [form (rf/subscribe [::form])
-        language (rf/subscribe [:language])
         loading? (rf/subscribe [::loading?])]
     (fn []
       [:div
@@ -91,4 +90,4 @@
        [document-title (text :t.administration/form)]
        (if @loading?
          [spinner/big]
-         [form-view @form @language])])))
+         [form-view @form])])))
diff --git a/src/cljs/rems/administration/forms.cljs b/src/cljs/rems/administration/forms.cljs
index 199c4bf1a..e3b168ab5 100644
--- a/src/cljs/rems/administration/forms.cljs
+++ b/src/cljs/rems/administration/forms.cljs
@@ -38,10 +38,12 @@
 
 (rf/reg-event-fx
  ::update-form
- (fn [_ [_ item description]]
+ (fn [_ [_ form description]]
    (status-modal/common-pending-handler! description)
    (put! "/api/forms/update"
-         {:params (select-keys item [:id :enabled :archived])
+         {:params {:id (:form/id form)
+                   :enabled (:enabled form)
+                   :archived (:archived form)}
           :handler (partial status-flags/common-update-handler! #(rf/dispatch [::fetch-forms]))
           :error-handler status-modal/common-error-handler!})
    {}))
@@ -60,19 +62,19 @@
    (text :t.administration/create-form)])
 
 (defn- to-view-form [form]
-   [:a.btn.btn-primary
-   {:href (str "/#/administration/forms/" (:id form))}
+  [:a.btn.btn-primary
+   {:href (str "/#/administration/forms/" (:form/id form))}
    (text :t.administration/view)])
 
 (defn- to-edit-form [form]
   [:button.btn.btn-primary
    {:type :button
-    :on-click #(rf/dispatch [:rems.administration.form/edit-form (:id form)])}
+    :on-click #(rf/dispatch [:rems.administration.form/edit-form (:form/id form)])}
    (text :t.administration/edit)])
 
 (defn- copy-as-new-form [form]
   [:a.btn.btn-primary
-   {:href (str "/#/administration/create-form/" (:id form))}
+   {:href (str "/#/administration/create-form/" (:form/id form))}
    (text :t.administration/copy-as-new)])
 
 (rf/reg-sub
@@ -81,9 +83,9 @@
    [(rf/subscribe [::forms])])
  (fn [[forms] _]
    (map (fn [form]
-          {:key (:id form)
-           :organization {:value (:organization form)}
-           :title {:value (:title form)}
+          {:key (:form/id form)
+           :organization {:value (:form/organization form)}
+           :title {:value (:form/title form)}
            :start (let [value (:start form)]
                     {:value value
                      :display-value (localize-time value)})
diff --git a/src/cljs/rems/administration/status_flags.cljs b/src/cljs/rems/administration/status_flags.cljs
index 3aa842f65..e2a0ed3bd 100644
--- a/src/cljs/rems/administration/status_flags.cljs
+++ b/src/cljs/rems/administration/status_flags.cljs
@@ -50,7 +50,8 @@
 
 (defn- format-update-error [{:keys [type catalogue-items resources workflows]}]
   (let [language @(rf/subscribe [:language])]
-    [:p (text type)
+    [:<>
+     [:p (text type)]
      (into [:ul]
            (for [ci catalogue-items]
              ;; TODO open in new tab?
diff --git a/src/cljs/rems/application.cljs b/src/cljs/rems/application.cljs
index 61f61ea27..3ad47d0bc 100644
--- a/src/cljs/rems/application.cljs
+++ b/src/cljs/rems/application.cljs
@@ -731,7 +731,7 @@
               :application/form {:form/fields [{:field/id 1
                                                 :field/type :text
                                                 :field/title {:en "Field 1"}
-                                                :field/placeholder {:en "prompt 1"}}
+                                                :field/placeholder {:en "placeholder 1"}}
                                                {:field/id 2
                                                 :field/type :label
                                                 :title "Please input your wishes below."}
@@ -739,11 +739,11 @@
                                                 :field/type :texta
                                                 :field/optional true
                                                 :field/title {:en "Field 2"}
-                                                :field/placeholder {:en "prompt 2"}}
+                                                :field/placeholder {:en "placeholder 2"}}
                                                {:field/id 4
                                                 :field/type :unsupported
                                                 :field/title {:en "Field 3"}
-                                                :field/placeholder {:en "prompt 3"}}
+                                                :field/placeholder {:en "placeholder 3"}}
                                                {:field/id 5
                                                 :field/type :date
                                                 :field/title {:en "Field 4"}}]}
@@ -767,7 +767,7 @@
               :application/form {:form/fields [{:field/id 1
                                                 :field/type :text
                                                 :field/title {:en "Field 1"}
-                                                :field/placeholder {:en "prompt 1"}}]}
+                                                :field/placeholder {:en "placeholder 1"}}]}
               :application/licenses [{:license/id 4
                                       :license/type :text
                                       :license/title {:en "A Text License"}
@@ -785,7 +785,7 @@
               :application/form {:form/fields [{:field/id 1
                                                 :field/type :text
                                                 :field/title {:en "Field 1"}
-                                                :field/placeholder {:en "prompt 1"}}]}
+                                                :field/placeholder {:en "placeholder 1"}}]}
               :application/licenses [{:license/id 4
                                       :license/type :text
                                       :license/title {:en "A Text License"}
diff --git a/src/cljs/rems/fields.cljs b/src/cljs/rems/fields.cljs
index 405bec6e9..7fe03f0a4 100644
--- a/src/cljs/rems/fields.cljs
+++ b/src/cljs/rems/fields.cljs
@@ -289,54 +289,54 @@
             [:form
              [field {:field/type :text
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}}]])
+                     :field/placeholder {:en "placeholder"}}]])
    (example "field of type \"text\" with maximum length"
             [:form
              [field {:field/type :text
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}
+                     :field/placeholder {:en "placeholder"}
                      :field/max-length 10}]])
    (example "field of type \"text\" with validation error"
             [:form
              [field {:field/type :text
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}
+                     :field/placeholder {:en "placeholder"}
                      :validation {:type :t.form.validation/required}}]])
    (example "non-editable field of type \"text\" without text"
             [:form
              [field {:field/type :text
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}
+                     :field/placeholder {:en "placeholder"}
                      :readonly true}]])
    (example "non-editable field of type \"text\" with text"
             [:form
              [field {:field/type :text
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}
+                     :field/placeholder {:en "placeholder"}
                      :readonly true
                      :field/value lipsum-short}]])
    (example "field of type \"texta\""
             [:form
              [field {:field/type :texta
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}}]])
+                     :field/placeholder {:en "placeholder"}}]])
    (example "field of type \"texta\" with maximum length"
             [:form
              [field {:field/type :texta
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}
+                     :field/placeholder {:en "placeholder"}
                      :field/max-length 10}]])
    (example "field of type \"texta\" with validation error"
             [:form
              [field {:field/type :texta
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}
+                     :field/placeholder {:en "placeholder"}
                      :validation {:type :t.form.validation/required}}]])
    (example "non-editable field of type \"texta\""
             [:form
              [field {:field/type :texta
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}
+                     :field/placeholder {:en "placeholder"}
                      :readonly true
                      :field/value lipsum-paragraphs}]])
    (let [previous-lipsum-paragraphs (-> lipsum-paragraphs
@@ -347,14 +347,14 @@
                [:form
                 [field {:field/type :texta
                         :field/title {:en "Title"}
-                        :field/placeholder {:en "prompt"}
+                        :field/placeholder {:en "placeholder"}
                         :field/value lipsum-paragraphs
                         :field/previous-value previous-lipsum-paragraphs}]])
       (example "editable field of type \"texta\" with previous value, diff shown"
                [:form
                 [field {:field/type :texta
                         :field/title {:en "Title"}
-                        :field/placeholder {:en "prompt"}
+                        :field/placeholder {:en "placeholder"}
                         :field/value lipsum-paragraphs
                         :field/previous-value previous-lipsum-paragraphs
                         :diff true}]])
@@ -362,7 +362,7 @@
                [:form
                 [field {:field/type :texta
                         :field/title {:en "Title"}
-                        :field/placeholder {:en "prompt"}
+                        :field/placeholder {:en "placeholder"}
                         :readonly true
                         :field/value lipsum-paragraphs
                         :field/previous-value previous-lipsum-paragraphs}]])
@@ -370,7 +370,7 @@
                [:form
                 [field {:field/type :texta
                         :field/title {:en "Title"}
-                        :field/placeholder {:en "prompt"}
+                        :field/placeholder {:en "placeholder"}
                         :readonly true
                         :field/value lipsum-paragraphs
                         :field/previous-value previous-lipsum-paragraphs
@@ -379,7 +379,7 @@
                [:form
                 [field {:field/type :texta
                         :field/title {:en "Title"}
-                        :field/placeholder {:en "prompt"}
+                        :field/placeholder {:en "placeholder"}
                         :readonly true
                         :field/value lipsum-paragraphs
                         :field/previous-value lipsum-paragraphs}]])])
@@ -501,7 +501,7 @@
              [field {:field/type :texta
                      :field/optional true
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}}]])
+                     :field/placeholder {:en "placeholder"}}]])
    (example "field of type \"label\""
             [:form
              [field {:field/type :label
@@ -510,4 +510,4 @@
             [:form
              [field {:field/type :description
                      :field/title {:en "Title"}
-                     :field/placeholder {:en "prompt"}}]])])
+                     :field/placeholder {:en "placeholder"}}]])])
diff --git a/src/cljs/rems/modal.cljs b/src/cljs/rems/modal.cljs
index 808a48598..24c287943 100644
--- a/src/cljs/rems/modal.cljs
+++ b/src/cljs/rems/modal.cljs
@@ -48,12 +48,7 @@
                   :title-class title-class
                   :always [:div.full
                            ;; max-height in order to keep header and controls visible
-                           [:div.modal--content
-                            {:style {:max-height "70vh" :overflow :auto}
-                             :ref (fn [elem] (when elem
-                                               (.focus elem)))
-                             :tabIndex "-1"}
-                            content]
+                           [:div.modal--content {:style {:max-height "70vh" :overflow :auto}} content]
                            (into [:div.modal--commands.commands {:style {:padding 0}}]
                                  commands)]
                   :open? true}]]


Test patch:
diff --git a/src/clj/rems/db/test_data.clj b/src/clj/rems/db/test_data.clj
index 1af25b020..5621d5ae7 100644
--- a/src/clj/rems/db/test_data.clj
+++ b/src/clj/rems/db/test_data.clj
@@ -98,7 +98,10 @@
 (defn- create-archived-form! []
   ;; only used from create-test-data!
   (let [yesterday (time/minus (time/now) (time/days 1))
-        id (:id (form/create-form! (+fake-users+ :owner) {:organization "nbn" :title "Archived form, should not be seen by applicants" :fields []}))]
+        id (:id (form/create-form! (+fake-users+ :owner)
+                                   {:form/organization "nbn"
+                                    :form/title "Archived form, should not be seen by applicants"
+                                    :form/fields []}))]
     (form/update-form! {:id id :enabled true :archived true})))
 
 (defn- create-expired-license! []
@@ -111,281 +114,281 @@
   [users]
   (:id (form/create-form!
         (users :owner)
-        {:organization "nbn"
-         :title "Basic form"
-         :fields [;; all form field types
-                  {:title {:en "Project name"
-                           :fi "Projektin nimi"}
-                   :optional false
-                   :type "description"
-                   :input-prompt {:en "Project"
-                                  :fi "Projekti"}}
-
-                  {:title {:en "Here would be some helpful instructions."
-                           :fi "Tässä olisi jotain täyttöohjeita."}
-                   :optional false
-                   :type "label"}
-
-                  {:title {:en "Purpose of the project"
-                           :fi "Projektin tarkoitus"}
-                   :optional false
-                   :type "texta"
-                   :input-prompt {:en "The purpose of the project is to..."
-                                  :fi "Projektin tarkoitus on..."}}
-
-                  {:title {:en "Start date of the project"
-                           :fi "Projektin aloituspäivä"}
-                   :optional true
-                   :type "date"}
-
-                  {:title {:en "Project plan"
-                           :fi "Projektisuunnitelma"}
-                   :optional true
-                   :type "attachment"}
-
-                  {:title {:en "Project team size"
-                           :fi "Projektitiimin koko"}
-                   :optional true
-                   :type "option"
-                   :options [{:key "1-5"
-                              :label {:en "1-5 persons"
-                                      :fi "1-5 henkilöä"}}
-                             {:key "6-20"
-                              :label {:en "6-20 persons"
-                                      :fi "6-20 henkilöä"}}
-                             {:key "20+"
-                              :label {:en "over 20 persons"
-                                      :fi "yli 20 henkilöä"}}]}
-
-                  {:title {:en "Where will the data be used?"
-                           :fi "Missä dataa tullaan käyttämään?"}
-                   :optional true
-                   :type "multiselect"
-                   :options [{:key "EU"
-                              :label {:en "Inside EU"
-                                      :fi "EU:n sisällä"}}
-                             {:key "USA"
-                              :label {:en "Inside USA"
-                                      :fi "Yhdysvalloissa"}}
-                             {:key "Other"
-                              :label {:en "Elsewhere"
-                                      :fi "Muualla"}}]}
-
-                  ;; fields which support maxlength
-                  {:title {:en "Project acronym"
-                           :fi "Projektin lyhenne"}
-                   :optional true
-                   :type "text"
-                   :maxlength 10}
-
-                  {:title {:en "Research plan"
-                           :fi "Tutkimussuunnitelma"}
-                   :optional true
-                   :type "texta"
-                   :maxlength 100}]})))
+        {:form/organization "nbn"
+         :form/title "Basic form"
+         :form/fields [;; all form field types
+                       {:field/title {:en "Project name"
+                                      :fi "Projektin nimi"}
+                        :field/optional false
+                        :field/type :description
+                        :field/placeholder {:en "Project"
+                                            :fi "Projekti"}}
+
+                       {:field/title {:en "Here would be some helpful instructions."
+                                      :fi "Tässä olisi jotain täyttöohjeita."}
+                        :field/optional false
+                        :field/type :label}
+
+                       {:field/title {:en "Purpose of the project"
+                                      :fi "Projektin tarkoitus"}
+                        :field/optional false
+                        :field/type :texta
+                        :field/placeholder {:en "The purpose of the project is to..."
+                                            :fi "Projektin tarkoitus on..."}}
+
+                       {:field/title {:en "Start date of the project"
+                                      :fi "Projektin aloituspäivä"}
+                        :field/optional true
+                        :field/type :date}
+
+                       {:field/title {:en "Project plan"
+                                      :fi "Projektisuunnitelma"}
+                        :field/optional true
+                        :field/type :attachment}
+
+                       {:field/title {:en "Project team size"
+                                      :fi "Projektitiimin koko"}
+                        :field/optional true
+                        :field/type :option
+                        :field/options [{:key "1-5"
+                                         :label {:en "1-5 persons"
+                                                 :fi "1-5 henkilöä"}}
+                                        {:key "6-20"
+                                         :label {:en "6-20 persons"
+                                                 :fi "6-20 henkilöä"}}
+                                        {:key "20+"
+                                         :label {:en "over 20 persons"
+                                                 :fi "yli 20 henkilöä"}}]}
+
+                       {:field/title {:en "Where will the data be used?"
+                                      :fi "Missä dataa tullaan käyttämään?"}
+                        :field/optional true
+                        :field/type :multiselect
+                        :field/options [{:key "EU"
+                                         :label {:en "Inside EU"
+                                                 :fi "EU:n sisällä"}}
+                                        {:key "USA"
+                                         :label {:en "Inside USA"
+                                                 :fi "Yhdysvalloissa"}}
+                                        {:key "Other"
+                                         :label {:en "Elsewhere"
+                                                 :fi "Muualla"}}]}
+
+                       ;; fields which support maxlength
+                       {:field/title {:en "Project acronym"
+                                      :fi "Projektin lyhenne"}
+                        :field/optional true
+                        :field/type :text
+                        :field/max-length 10}
+
+                       {:field/title {:en "Research plan"
+                                      :fi "Tutkimussuunnitelma"}
+                        :field/optional true
+                        :field/type :texta
+                        :field/max-length 100}]})))
 
 (defn create-thl-demo-form!
   [users]
   (:id (form/create-form!
         (users :owner)
-        {:organization "nbn"
-         :title "THL form"
-         :fields [{:title {:en "Application title"
-                           :fi "Hakemuksen otsikko"}
-                   :optional true
-                   :type "description"
-                   :input-prompt {:en "Study of.."
-                                  :fi "Tutkimus aiheesta.."}}
-                  {:title {:en "1. Research project full title"
-                           :fi "1. Tutkimusprojektin täysi nimi"}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "2. This is an amendment of a previous approved application"
-                           :fi "2. Hakemus täydentää edellistä hakemusta"}
-                   :optional false
-                   :type "option"
-                   :options [{:key "false"
-                              :label {:en "no"
-                                      :fi "ei"}}
-                             {:key "true"
-                              :label {:en "yes"
-                                      :fi "kyllä"}}]}
-                  {:title {:en "If yes, what were the previous project permit code/s?"
-                           :fi "Jos kyllä, mitkä olivat edelliset projektin lupakoodit?"}
-                   :optional true
-                   :type "text"}
-                  {:title {:en "3. Study PIs (name, titile, affiliation, email)"
-                           :fi "3. Henkilöstö (nimi, titteli, yhteys projektiin, sähköposti)"}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "4. Contact person for application if different than applicant (name, email)"
-                           :fi "4. Yhteyshenkilö, jos ei sama kuin hakija (nimi, sähköposti)"}
-                   :optional true
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "5. Research project start date"
-                           :fi "5. Projektin aloituspäivä"}
-                   :optional false
-                   :type "date"}
-                  {:title {:en "6. Research project end date"
-                           :fi "6. Projektin lopetuspäivä"}
-                   :optional false
-                   :type "date"}
-                  {:title {:en "7. Describe in detail the aims of the study and analysis plan"
-                           :fi "7. Kuvaile yksityiskohtaisesti tutkimussuunnitelma"}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "8. If this is an amendment, please describe briefly what is new"
-                           :fi "8. Jos tämä on täydennys edelliseen hakemukseen, kuvaile tiiviisti, mikä on muuttunut."}
-                   :optional true
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "9. Public description of the project (in Finnish, when possible), to be published in THL Biobank."
-                           :fi "9. Kuvaile yksityiskohtaisesti tutkimussuunnitelma"}
-                   :input-prompt {:en "Meant for sample donors and for anyone interested in the research done using THL Biobank's sample collections. This summary and the name of the Study PI will be published in THL Biobank's web pages."
-                                  :fi "Tarkoitettu aineistojen lahjoittajille ja kaikille, joita kiinnostaa THL:n Biopankkia käyttävät tutkimusprojektit. Tämä kuvaus sekä tutkijan nimi julkaistaan THL:n nettisivuilla, kun sopimus on allekirjoitettu."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "10. Place/plces of research, including place of sample and/or data analysis."
-                           :fi "10. Tutkimuksen yysinen sijainti, mukaanlukien paikka, missä data-analyysi toteutetaan."}
-                   :input-prompt {:en "List all research center involved in this study, and each center's role. Specify which centers will analyze which data and/or samples.."
-                                  :fi "Listaa kaikki tutkimuskeskukset, jotka osallistuvat tähän tutkimukseen, ml. niiden roolit tutkimuksessa. Erittele, missä analysoidaan mikäkin näyte."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "11. Description of other research group members and their role in the applied project."
-                           :fi "11. Kuvaus muista tutkimukseen osallistuvista henkilöistä, ja heidän roolistaan projektissa."}
-                   :input-prompt {:en "For every group member: name, title, affiliation, contact information. In addition describe earch member's role in the project (e.g. cohor representative, data analyst, etc.)"
-                                  :fi "Anna jokaisesta jäsenestä: nimi, titteli, yhteys projektiin, yhteystiedot. Kuvaile lisäki jokaisen henkilön rooli projektissa."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "12. Specify selection criteria of study participants (if applicable)"
-                           :fi "12. Erottele tukimuksen osallistujien valintakriteerit (jos käytetty)"}
-                   :input-prompt {:en "Describe any specific criteria by which study participans will be selected. For example, selection for specific age group, gender, area/locality, disease status etc."
-                                  :fi "Kuvaa tarkat valintakriteerit, joilla tutkimuksen osallistujat valitaan. Esimerkiksi ikäryhmä, sukupuoli, alue, taudin tila jne."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "13. Specify requested phenotype data (information on variables is found at https://kite.fimm.fi)"
-                           :fi "13. Tarkenna pyydetty fenotyyppidatta (tietoa muuttujista on saatavilla osoitteesta https://kite.fimm.fi)"}
-                   :input-prompt {:en "Desrcibe in detail the phenotype data needed for the study. Lists of variables are to be attached to the application (below)."
-                                  :fi "Kuvaile yksityiskohtaisesti tutkimukseen tarvittava fenotyyppidata. Lista muuttujista lisätään hakemukseen liitteenä."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "14. Specify requested genomics or other omics data (if applicable)"
-                           :fi "14. Kuvaile tarvittava genomiikkadata."}
-                   :input-prompt {:en "Specify in detail the requested data format for different genomics or other omics data types. Information of available omics data is found at THL Biobank web page (www.thl.fi/biobank/researchers)"
-                                  :fi "Kuvaile tarvitsemasi genomiikkadata. Lisätietoa saatavilla osoitteesta www.thl.fi/biobank/researchers"}
-                   :optional true
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "16. Are biological samples requested?"
-                           :fi "16. Pyydetäänkö biologisia näytteitä?"}
-                   :optional false
-                   :type "option"
-                   :options [{:key "false"
-                              :label {:en "no"
-                                      :fi "ei"}}
-                             {:key "true"
-                              :label {:en "yes"
-                                      :fi "kyllä"}}]}
-                  {:title {:en "The type and amount of biological samples requested"
-                           :fi "Biologisten näytteiden tyypit ja määrät."}
-                   :input-prompt {:en "Type and amount of samples and any additional specific criteria."
-                                  :fi "Biologisten näytteiden määrät, tyypit, ja mahdolliset muut kriteerit."}
-                   :optional true
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "17. What study results will be returned to THL Biobank (if any)?"
-                           :fi "17. Mitä tutkimustuloksia tullaan palauttamaan THL Biopankkiin?"}
-                   :input-prompt {:en "Study results such as new laboratory measurements, produced omics data and other analysis data (\"raw data\")"
-                                  :fi "Tutkimustuloksia kuten mittaustuloksia, uutta biologista dataa, tai muita analyysien tuloksia (\"raaka-dataa\")"}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "Expected date for return of study results"
-                           :fi "Odotettu tutkimustuloksien palautuspäivämäärä"}
-                   :optional true
-                   :type "date"}
-                  {:title {:en "18. Ethical aspects of the project"
-                           :fi "18. Tutkimuksen eettiset puolet"}
-                   :input-prompt {:en "If you have any documents from an ethical board, please provide them as an attachment."
-                                  :fi "Liitä mahdolliset eettisen toimikunnan lausunnot hakemuksen loppuun."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "19. Project keywords (max 5)"
-                           :fi "19. Projektin avainsanat (maks. 5)"}
-                   :input-prompt {:en "List a few keywords that are related to this research project (please separate with comma)"
-                                  :fi "Listaa muutama projektiin liittyvä avainsana, pilkuilla erotettuina."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "20. Planned publications (max 3)"
-                           :fi "20. Suunnitellut julkaisut (maks. 3)"}
-                   :input-prompt {:en "Planned publication titles / research topics"
-                                  :fi "Suunniteltujen julkaisujen otsikot / tutkimusaiheet"}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "21. Funding information"
-                           :fi "21. Rahoitus"}
-                   :input-prompt {:en "List all funding sources which will be used for this research project."
-                                  :fi "Listaa kaikki rahoituslähteet joita tullaan käyttämään tähän tutkimusprojektiin"}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "22. Invoice address (Service prices: www.thl.fi/biobank/researchers)"
-                           :fi "22. Laskutusosoite (Palveluhinnasto: www.thl.fi/biobank/researchers)"}
-                   :input-prompt {:en "Electronic invoice address when possible + invoicing reference"
-                                  :fi "Sähköinen laskutus, kun mahdollista. Lisäksi viitenumero."}
-                   :optional false
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "23. Other information"
-                           :fi "23. Muuta"}
-                   :input-prompt {:en "Any other relevant information for the application"
-                                  :fi "Muuta hakemukseen liittyvää oleellista tietoa"}
-                   :optional true
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "THL Biobank's registered area/s of operation to which the research project complies:"
-                           :fi "THL Biobankin toimialueet, joihin tutkimusprojekti liittyy:"}
-                   :optional false
-                   :type "multiselect"
-                   :options [{:key "population_health"
-                              :label {:en "Promoting the population's health"
-                                      :fi "Edistää kansanterveytttä"}}
-                             {:key "disease_mechanisms"
-                              :label {:en "Identifying factors involved in disease mechanisms"
-                                      :fi "Tunnistaa tautien mekanismeja"}}
-                             {:key "disease_prevention"
-                              :label {:en "Disease prevention"
-                                      :fi "Estää tautien leviämistä"}}
-                             {:key "health_product_development"
-                              :label {:en "Developing products that promote the welfare and health of the population"
-                                      :fi "Kehittää tuotteita, jotka edistävät kansanterveyttä."}}
-                             {:key "treatment_development"
-                              :label {:en "Developing products and treatments for diseases"
-                                      :fi "Kehittää tuotteita ja parannuskeinoja tautien varalle"}}
-                             {:key "other"
-                              :label {:en "Other"
-                                      :fi "Muuta"}}]}
-                  {:title {:en "Other, specify"
-                           :fi "Muuta, tarkenna"}
-                   :optional true
-                   :type "texta"
-                   :maxlength 100}
-                  {:title {:en "Data management plan (pdf)"
-                           :fi "Datanhallintasuunnitelma (pdf)"}
-                   :optional true
-                   :type "attachment"}]})))
+        {:form/organization "nbn"
+         :form/title "THL form"
+         :form/fields [{:field/title {:en "Application title"
+                                      :fi "Hakemuksen otsikko"}
+                        :field/optional true
+                        :field/type :description
+                        :field/placeholder {:en "Study of.."
+                                            :fi "Tutkimus aiheesta.."}}
+                       {:field/title {:en "1. Research project full title"
+                                      :fi "1. Tutkimusprojektin täysi nimi"}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "2. This is an amendment of a previous approved application"
+                                      :fi "2. Hakemus täydentää edellistä hakemusta"}
+                        :field/optional false
+                        :field/type :option
+                        :field/options [{:key "false"
+                                         :label {:en "no"
+                                                 :fi "ei"}}
+                                        {:key "true"
+                                         :label {:en "yes"
+                                                 :fi "kyllä"}}]}
+                       {:field/title {:en "If yes, what were the previous project permit code/s?"
+                                      :fi "Jos kyllä, mitkä olivat edelliset projektin lupakoodit?"}
+                        :field/optional true
+                        :field/type :text}
+                       {:field/title {:en "3. Study PIs (name, titile, affiliation, email)"
+                                      :fi "3. Henkilöstö (nimi, titteli, yhteys projektiin, sähköposti)"}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "4. Contact person for application if different than applicant (name, email)"
+                                      :fi "4. Yhteyshenkilö, jos ei sama kuin hakija (nimi, sähköposti)"}
+                        :field/optional true
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "5. Research project start date"
+                                      :fi "5. Projektin aloituspäivä"}
+                        :field/optional false
+                        :field/type :date}
+                       {:field/title {:en "6. Research project end date"
+                                      :fi "6. Projektin lopetuspäivä"}
+                        :field/optional false
+                        :field/type :date}
+                       {:field/title {:en "7. Describe in detail the aims of the study and analysis plan"
+                                      :fi "7. Kuvaile yksityiskohtaisesti tutkimussuunnitelma"}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "8. If this is an amendment, please describe briefly what is new"
+                                      :fi "8. Jos tämä on täydennys edelliseen hakemukseen, kuvaile tiiviisti, mikä on muuttunut."}
+                        :field/optional true
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "9. Public description of the project (in Finnish, when possible), to be published in THL Biobank."
+                                      :fi "9. Kuvaile yksityiskohtaisesti tutkimussuunnitelma"}
+                        :field/placeholder {:en "Meant for sample donors and for anyone interested in the research done using THL Biobank's sample collections. This summary and the name of the Study PI will be published in THL Biobank's web pages."
+                                            :fi "Tarkoitettu aineistojen lahjoittajille ja kaikille, joita kiinnostaa THL:n Biopankkia käyttävät tutkimusprojektit. Tämä kuvaus sekä tutkijan nimi julkaistaan THL:n nettisivuilla, kun sopimus on allekirjoitettu."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "10. Place/plces of research, including place of sample and/or data analysis."
+                                      :fi "10. Tutkimuksen yysinen sijainti, mukaanlukien paikka, missä data-analyysi toteutetaan."}
+                        :field/placeholder {:en "List all research center involved in this study, and each center's role. Specify which centers will analyze which data and/or samples.."
+                                            :fi "Listaa kaikki tutkimuskeskukset, jotka osallistuvat tähän tutkimukseen, ml. niiden roolit tutkimuksessa. Erittele, missä analysoidaan mikäkin näyte."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "11. Description of other research group members and their role in the applied project."
+                                      :fi "11. Kuvaus muista tutkimukseen osallistuvista henkilöistä, ja heidän roolistaan projektissa."}
+                        :field/placeholder {:en "For every group member: name, title, affiliation, contact information. In addition describe earch member's role in the project (e.g. cohor representative, data analyst, etc.)"
+                                            :fi "Anna jokaisesta jäsenestä: nimi, titteli, yhteys projektiin, yhteystiedot. Kuvaile lisäki jokaisen henkilön rooli projektissa."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "12. Specify selection criteria of study participants (if applicable)"
+                                      :fi "12. Erottele tukimuksen osallistujien valintakriteerit (jos käytetty)"}
+                        :field/placeholder {:en "Describe any specific criteria by which study participans will be selected. For example, selection for specific age group, gender, area/locality, disease status etc."
+                                            :fi "Kuvaa tarkat valintakriteerit, joilla tutkimuksen osallistujat valitaan. Esimerkiksi ikäryhmä, sukupuoli, alue, taudin tila jne."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "13. Specify requested phenotype data (information on variables is found at https://kite.fimm.fi)"
+                                      :fi "13. Tarkenna pyydetty fenotyyppidatta (tietoa muuttujista on saatavilla osoitteesta https://kite.fimm.fi)"}
+                        :field/placeholder {:en "Desrcibe in detail the phenotype data needed for the study. Lists of variables are to be attached to the application (below)."
+                                            :fi "Kuvaile yksityiskohtaisesti tutkimukseen tarvittava fenotyyppidata. Lista muuttujista lisätään hakemukseen liitteenä."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "14. Specify requested genomics or other omics data (if applicable)"
+                                      :fi "14. Kuvaile tarvittava genomiikkadata."}
+                        :field/placeholder {:en "Specify in detail the requested data format for different genomics or other omics data types. Information of available omics data is found at THL Biobank web page (www.thl.fi/biobank/researchers)"
+                                            :fi "Kuvaile tarvitsemasi genomiikkadata. Lisätietoa saatavilla osoitteesta www.thl.fi/biobank/researchers"}
+                        :field/optional true
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "16. Are biological samples requested?"
+                                      :fi "16. Pyydetäänkö biologisia näytteitä?"}
+                        :field/optional false
+                        :field/type :option
+                        :field/options [{:key "false"
+                                         :label {:en "no"
+                                                 :fi "ei"}}
+                                        {:key "true"
+                                         :label {:en "yes"
+                                                 :fi "kyllä"}}]}
+                       {:field/title {:en "The type and amount of biological samples requested"
+                                      :fi "Biologisten näytteiden tyypit ja määrät."}
+                        :field/placeholder {:en "Type and amount of samples and any additional specific criteria."
+                                            :fi "Biologisten näytteiden määrät, tyypit, ja mahdolliset muut kriteerit."}
+                        :field/optional true
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "17. What study results will be returned to THL Biobank (if any)?"
+                                      :fi "17. Mitä tutkimustuloksia tullaan palauttamaan THL Biopankkiin?"}
+                        :field/placeholder {:en "Study results such as new laboratory measurements, produced omics data and other analysis data (\"raw data\")"
+                                            :fi "Tutkimustuloksia kuten mittaustuloksia, uutta biologista dataa, tai muita analyysien tuloksia (\"raaka-dataa\")"}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "Expected date for return of study results"
+                                      :fi "Odotettu tutkimustuloksien palautuspäivämäärä"}
+                        :field/optional true
+                        :field/type :date}
+                       {:field/title {:en "18. Ethical aspects of the project"
+                                      :fi "18. Tutkimuksen eettiset puolet"}
+                        :field/placeholder {:en "If you have any documents from an ethical board, please provide them as an attachment."
+                                            :fi "Liitä mahdolliset eettisen toimikunnan lausunnot hakemuksen loppuun."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "19. Project keywords (max 5)"
+                                      :fi "19. Projektin avainsanat (maks. 5)"}
+                        :field/placeholder {:en "List a few keywords that are related to this research project (please separate with comma)"
+                                            :fi "Listaa muutama projektiin liittyvä avainsana, pilkuilla erotettuina."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "20. Planned publications (max 3)"
+                                      :fi "20. Suunnitellut julkaisut (maks. 3)"}
+                        :field/placeholder {:en "Planned publication titles / research topics"
+                                            :fi "Suunniteltujen julkaisujen otsikot / tutkimusaiheet"}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "21. Funding information"
+                                      :fi "21. Rahoitus"}
+                        :field/placeholder {:en "List all funding sources which will be used for this research project."
+                                            :fi "Listaa kaikki rahoituslähteet joita tullaan käyttämään tähän tutkimusprojektiin"}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "22. Invoice address (Service prices: www.thl.fi/biobank/researchers)"
+                                      :fi "22. Laskutusosoite (Palveluhinnasto: www.thl.fi/biobank/researchers)"}
+                        :field/placeholder {:en "Electronic invoice address when possible + invoicing reference"
+                                            :fi "Sähköinen laskutus, kun mahdollista. Lisäksi viitenumero."}
+                        :field/optional false
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "23. Other information"
+                                      :fi "23. Muuta"}
+                        :field/placeholder {:en "Any other relevant information for the application"
+                                            :fi "Muuta hakemukseen liittyvää oleellista tietoa"}
+                        :field/optional true
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "THL Biobank's registered area/s of operation to which the research project complies:"
+                                      :fi "THL Biobankin toimialueet, joihin tutkimusprojekti liittyy:"}
+                        :field/optional false
+                        :field/type :multiselect
+                        :field/options [{:key "population_health"
+                                         :label {:en "Promoting the population's health"
+                                                 :fi "Edistää kansanterveytttä"}}
+                                        {:key "disease_mechanisms"
+                                         :label {:en "Identifying factors involved in disease mechanisms"
+                                                 :fi "Tunnistaa tautien mekanismeja"}}
+                                        {:key "disease_prevention"
+                                         :label {:en "Disease prevention"
+                                                 :fi "Estää tautien leviämistä"}}
+                                        {:key "health_product_development"
+                                         :label {:en "Developing products that promote the welfare and health of the population"
+                                                 :fi "Kehittää tuotteita, jotka edistävät kansanterveyttä."}}
+                                        {:key "treatment_development"
+                                         :label {:en "Developing products and treatments for diseases"
+                                                 :fi "Kehittää tuotteita ja parannuskeinoja tautien varalle"}}
+                                        {:key "other"
+                                         :label {:en "Other"
+                                                 :fi "Muuta"}}]}
+                       {:field/title {:en "Other, specify"
+                                      :fi "Muuta, tarkenna"}
+                        :field/optional true
+                        :field/type :texta
+                        :field/max-length 100}
+                       {:field/title {:en "Data management plan (pdf)"
+                                      :fi "Datanhallintasuunnitelma (pdf)"}
+                        :field/optional true
+                        :field/type :attachment}]})))
 
 (defn- create-workflows! [users]
   (let [approver1 (users :approver1)
@@ -650,6 +653,9 @@
         application (create-draft! applicant item-without-new-license wfid "applied before license was valid" (time/minus (time/now) (time/days 2)))]
     (legacy/submit-application applicant application)))
 
+(defn- assert-no-error [error]
+  (assert (nil? error) {:error error}))
+
 (defn create-performance-test-data! []
   (let [resource-count 1000
         application-count 1000
@@ -664,21 +670,21 @@
                                                      :handlers handlers}))
         form-id (:id (form/create-form!
                       owner
-                      {:organization "perf"
-                       :title "Performance tests"
-                       :fields [{:title {:en "Project name"
-                                         :fi "Projektin nimi"}
-                                 :optional false
-                                 :type "description"
-                                 :input-prompt {:en "Project"
-                                                :fi "Projekti"}}
-
-                                {:title {:en "Project description"
-                                         :fi "Projektin kuvaus"}
-                                 :optional false
-                                 :type "texta"
-                                 :input-prompt {:en "The purpose of the project is to..."
-                                                :fi "Projektin tarkoitus on..."}}]}))
+                      {:form/organization "perf"
+                       :form/title "Performance tests"
+                       :form/fields [{:field/title {:en "Project name"
+                                                    :fi "Projektin nimi"}
+                                      :field/optional false
+                                      :field/type :description
+                                      :field/placeholder {:en "Project"
+                                                          :fi "Projekti"}}
+
+                                     {:field/title {:en "Project description"
+                                                    :fi "Projektin kuvaus"}
+                                      :field/optional false
+                                      :field/type :texta
+                                      :field/placeholder {:en "The purpose of the project is to..."
+                                                          :fi "Projektin tarkoitus on..."}}]}))
         form (form/get-form-template form-id)
         license-id (:id (licenses/create-license!
                          {:licensetype "text"
@@ -713,30 +719,33 @@
             user-id (rand-nth user-ids)
             handler (rand-nth handlers)
             app-id (:application-id (applications/create-application! user-id [cat-item-id]))]
-        (assert (nil? (applications/command!
-                       {:type :application.command/save-draft
-                        :actor user-id
-                        :time (time/now)
-                        :application-id app-id
-                        :field-values [{:field (:id (first (:fields form)))
-                                        :value (str "Performance test application " (UUID/randomUUID))}
-                                       {:field (:id (second (:fields form)))
-                                        ;; 5000 characters (10 KB) of lorem ipsum generated with www.lipsum.com
-                                        ;; to increase the memory requirements of an application
-                                        :value (str "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut non diam vel erat dapibus facilisis vel vitae nunc. Curabitur at fermentum lorem. Cras et bibendum ante. Etiam convallis erat justo. Phasellus cursus molestie vehicula. Etiam molestie tellus vitae consectetur dignissim. Pellentesque euismod hendrerit mi sed tincidunt. Integer quis lorem ut ipsum egestas hendrerit. Aenean est nunc, mattis euismod erat in, sodales rutrum mauris. Praesent sit amet risus quis felis congue ultricies. Nulla facilisi. Sed mollis justo id tristique volutpat.\n\nPhasellus augue mi, facilisis ac velit et, pharetra tristique nunc. Pellentesque eget arcu quam. Curabitur dictum nulla varius hendrerit varius. Proin vulputate, ex lacinia commodo varius, ipsum velit viverra est, eget molestie dui nisi non eros. Nulla lobortis odio a magna mollis placerat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer consectetur libero ut gravida ullamcorper. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec aliquam feugiat mollis. Quisque massa lacus, efficitur vel justo vel, elementum mollis magna. Maecenas at sem sem. Praesent sed ex mattis, egestas dui non, volutpat lorem. Nulla tempor, nisi rutrum accumsan varius, tellus elit faucibus nulla, vel mattis lacus justo at ante. Sed ut mollis ex, sed tincidunt ex.\n\nMauris laoreet nibh eget erat tincidunt pharetra. Aenean sagittis maximus consectetur. Curabitur interdum nibh sed tincidunt finibus. Sed blandit nec lorem at iaculis. Morbi non augue nec tortor hendrerit mollis ut non arcu. Suspendisse maximus nec ligula a efficitur. Etiam ultrices rhoncus leo quis dapibus. Integer vel rhoncus est. Integer blandit varius auctor. Vestibulum suscipit suscipit risus, sit amet venenatis lacus iaculis a. Duis eu turpis sit amet nibh sagittis convallis at quis ligula. Sed eget justo quis risus iaculis lacinia vitae a justo. In hac habitasse platea dictumst. Maecenas euismod et lorem vel viverra.\n\nDonec bibendum nec ipsum in volutpat. Vivamus in elit venenatis, venenatis libero ac, ultrices dolor. Morbi quis odio in neque consequat rutrum. Suspendisse quis sapien id sapien fermentum dignissim. Nam eu est vel risus volutpat mollis sed quis eros. Proin leo nulla, dictum id hendrerit vitae, scelerisque in elit. Proin consectetur sodales arcu ac tristique. Suspendisse ut elementum ligula, at rhoncus mauris. Aliquam lacinia at diam eget mattis. Phasellus quam leo, hendrerit sit amet mi eget, porttitor aliquet velit. Proin turpis ante, consequat in enim nec, tempus consequat magna. Vestibulum fringilla ac turpis nec malesuada. Proin id lectus iaculis, suscipit erat at, volutpat turpis. In quis faucibus elit, ut maximus nibh. Sed egestas egestas dolor.\n\nNulla varius orci quam, id auctor enim ultrices nec. Morbi et tellus ac metus sodales convallis sed vehicula neque. Pellentesque rhoncus mattis massa a bibendum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce tincidunt nulla non aliquet facilisis. Praesent nisl nisi, finibus id odio sed, consectetur feugiat mauris. Suspendisse sed lacinia ligula. Duis vitae nisl leo. Donec erat arcu, feugiat sit amet sagittis ac, scelerisque nec est. Pellentesque finibus mauris nulla, in maximus sapien pharetra vitae. Sed leo elit, consequat eu aliquam vitae, feugiat ut eros. Pellentesque dictum feugiat odio sed commodo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin neque quam, varius vel libero sit amet, rhoncus sollicitudin ex. In a dui non neque malesuada pellentesque.\n\nProin tincidunt nisl non commodo faucibus. Sed porttitor arcu neque, vitae bibendum sapien placerat nec. Integer eget tristique orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec eu molestie eros. Nunc iaculis rhoncus enim, vel mattis felis fringilla condimentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean ac augue nulla. Phasellus vitae nulla lobortis, mattis magna ac, gravida ipsum. Aenean ornare non nunc non luctus. Aenean lacinia lectus nec velit finibus egestas vel ut ipsum. Cras hendrerit rhoncus erat, vel maximus nunc.\n\nPraesent quis imperdiet quam. Praesent ligula tellus, consectetur sed lacus eu, malesuada condimentum tellus. Donec et diam hendrerit, dictum diam quis, aliquet purus. Suspendisse pulvinar neque at efficitur iaculis. Nulla erat orci, euismod id velit sed, dictum hendrerit arcu. Nulla aliquam molestie aliquam. Duis et semper nisi, eget commodo arcu. Praesent rhoncus, nulla id sodales eleifend, ante ipsum pellentesque augue, id iaculis sem est vitae est. Phasellus cursus diam a lorem vestibulum sodales. Nullam lacinia tortor vel tellus commodo, sit amet sodales quam malesuada.\n\nNulla tempor lectus vel arcu feugiat, vel dapibus ex dapibus. Maecenas purus justo, aliquet et sem sit amet, tincidunt venenatis dui. Nulla eget purus id sapien elementum rutrum eu vel libero. Cras non accumsan justo posuere.\n\n"
-                                                    ;; prevent string interning, just to be sure
-                                                    (UUID/randomUUID))}]})))
-        (assert (nil? (applications/command!
-                       {:type :application.command/submit
-                        :actor user-id
-                        :time (time/now)
-                        :application-id app-id})))
-        (assert (nil? (applications/command!
-                       {:type :application.command/approve
-                        :actor handler
-                        :time (time/now)
-                        :application-id app-id
-                        :comment "Looks fine."})))))))
+        (assert-no-error
+         (applications/command!
+          {:type :application.command/save-draft
+           :actor user-id
+           :time (time/now)
+           :application-id app-id
+           :field-values [{:field (:field/id (first (:form/fields form)))
+                           :value (str "Performance test application " (UUID/randomUUID))}
+                          {:field (:field/id (second (:form/fields form)))
+                           ;; 5000 characters (10 KB) of lorem ipsum generated with www.lipsum.com
+                           ;; to increase the memory requirements of an application
+                           :value (str "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Ut non diam vel erat dapibus facilisis vel vitae nunc. Curabitur at fermentum lorem. Cras et bibendum ante. Etiam convallis erat justo. Phasellus cursus molestie vehicula. Etiam molestie tellus vitae consectetur dignissim. Pellentesque euismod hendrerit mi sed tincidunt. Integer quis lorem ut ipsum egestas hendrerit. Aenean est nunc, mattis euismod erat in, sodales rutrum mauris. Praesent sit amet risus quis felis congue ultricies. Nulla facilisi. Sed mollis justo id tristique volutpat.\n\nPhasellus augue mi, facilisis ac velit et, pharetra tristique nunc. Pellentesque eget arcu quam. Curabitur dictum nulla varius hendrerit varius. Proin vulputate, ex lacinia commodo varius, ipsum velit viverra est, eget molestie dui nisi non eros. Nulla lobortis odio a magna mollis placerat. Interdum et malesuada fames ac ante ipsum primis in faucibus. Integer consectetur libero ut gravida ullamcorper. Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Donec aliquam feugiat mollis. Quisque massa lacus, efficitur vel justo vel, elementum mollis magna. Maecenas at sem sem. Praesent sed ex mattis, egestas dui non, volutpat lorem. Nulla tempor, nisi rutrum accumsan varius, tellus elit faucibus nulla, vel mattis lacus justo at ante. Sed ut mollis ex, sed tincidunt ex.\n\nMauris laoreet nibh eget erat tincidunt pharetra. Aenean sagittis maximus consectetur. Curabitur interdum nibh sed tincidunt finibus. Sed blandit nec lorem at iaculis. Morbi non augue nec tortor hendrerit mollis ut non arcu. Suspendisse maximus nec ligula a efficitur. Etiam ultrices rhoncus leo quis dapibus. Integer vel rhoncus est. Integer blandit varius auctor. Vestibulum suscipit suscipit risus, sit amet venenatis lacus iaculis a. Duis eu turpis sit amet nibh sagittis convallis at quis ligula. Sed eget justo quis risus iaculis lacinia vitae a justo. In hac habitasse platea dictumst. Maecenas euismod et lorem vel viverra.\n\nDonec bibendum nec ipsum in volutpat. Vivamus in elit venenatis, venenatis libero ac, ultrices dolor. Morbi quis odio in neque consequat rutrum. Suspendisse quis sapien id sapien fermentum dignissim. Nam eu est vel risus volutpat mollis sed quis eros. Proin leo nulla, dictum id hendrerit vitae, scelerisque in elit. Proin consectetur sodales arcu ac tristique. Suspendisse ut elementum ligula, at rhoncus mauris. Aliquam lacinia at diam eget mattis. Phasellus quam leo, hendrerit sit amet mi eget, porttitor aliquet velit. Proin turpis ante, consequat in enim nec, tempus consequat magna. Vestibulum fringilla ac turpis nec malesuada. Proin id lectus iaculis, suscipit erat at, volutpat turpis. In quis faucibus elit, ut maximus nibh. Sed egestas egestas dolor.\n\nNulla varius orci quam, id auctor enim ultrices nec. Morbi et tellus ac metus sodales convallis sed vehicula neque. Pellentesque rhoncus mattis massa a bibendum. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia Curae; Fusce tincidunt nulla non aliquet facilisis. Praesent nisl nisi, finibus id odio sed, consectetur feugiat mauris. Suspendisse sed lacinia ligula. Duis vitae nisl leo. Donec erat arcu, feugiat sit amet sagittis ac, scelerisque nec est. Pellentesque finibus mauris nulla, in maximus sapien pharetra vitae. Sed leo elit, consequat eu aliquam vitae, feugiat ut eros. Pellentesque dictum feugiat odio sed commodo. Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin neque quam, varius vel libero sit amet, rhoncus sollicitudin ex. In a dui non neque malesuada pellentesque.\n\nProin tincidunt nisl non commodo faucibus. Sed porttitor arcu neque, vitae bibendum sapien placerat nec. Integer eget tristique orci. Class aptent taciti sociosqu ad litora torquent per conubia nostra, per inceptos himenaeos. Donec eu molestie eros. Nunc iaculis rhoncus enim, vel mattis felis fringilla condimentum. Interdum et malesuada fames ac ante ipsum primis in faucibus. Aenean ac augue nulla. Phasellus vitae nulla lobortis, mattis magna ac, gravida ipsum. Aenean ornare non nunc non luctus. Aenean lacinia lectus nec velit finibus egestas vel ut ipsum. Cras hendrerit rhoncus erat, vel maximus nunc.\n\nPraesent quis imperdiet quam. Praesent ligula tellus, consectetur sed lacus eu, malesuada condimentum tellus. Donec et diam hendrerit, dictum diam quis, aliquet purus. Suspendisse pulvinar neque at efficitur iaculis. Nulla erat orci, euismod id velit sed, dictum hendrerit arcu. Nulla aliquam molestie aliquam. Duis et semper nisi, eget commodo arcu. Praesent rhoncus, nulla id sodales eleifend, ante ipsum pellentesque augue, id iaculis sem est vitae est. Phasellus cursus diam a lorem vestibulum sodales. Nullam lacinia tortor vel tellus commodo, sit amet sodales quam malesuada.\n\nNulla tempor lectus vel arcu feugiat, vel dapibus ex dapibus. Maecenas purus justo, aliquet et sem sit amet, tincidunt venenatis dui. Nulla eget purus id sapien elementum rutrum eu vel libero. Cras non accumsan justo posuere.\n\n"
+                                       ;; prevent string interning, just to be sure
+                                       (UUID/randomUUID))}]}))
+        (assert-no-error
+         (applications/command!
+          {:type :application.command/submit
+           :actor user-id
+           :time (time/now)
+           :application-id app-id}))
+        (assert-no-error
+         (applications/command!
+          {:type :application.command/approve
+           :actor handler
+           :time (time/now)
+           :application-id app-id
+           :comment "Looks fine."}))))))
 
 (defn create-test-data! []
   (DateTimeUtils/setCurrentMillisFixed (.getMillis creation-time))
diff --git a/test/clj/rems/api/test_administration.clj b/test/clj/rems/api/test_administration.clj
index c79bb1e56..41f227662 100644
--- a/test/clj/rems/api/test_administration.clj
+++ b/test/clj/rems/api/test_administration.clj
@@ -20,7 +20,7 @@
                                   api-key user-id))
         resource-id (:id (api-call :post "/api/resources/create" {:resid "test" :organization "abc" :licenses [license-id]}
                                    api-key user-id))
-        form-id (:id (api-call :post "/api/forms/create" {:organization "abc" :title "form update test" :fields []}
+        form-id (:id (api-call :post "/api/forms/create" {:form/organization "abc" :form/title "form update test" :form/fields []}
                                api-key user-id))
         workflow-id (:id (api-call :post "/api/workflows/create"
                                    {:organization "abc" :title "dynamic workflow"
diff --git a/test/clj/rems/api/test_applications.clj b/test/clj/rems/api/test_applications.clj
index 4f59ec0f4..594282a1f 100644
--- a/test/clj/rems/api/test_applications.clj
+++ b/test/clj/rems/api/test_applications.clj
@@ -26,9 +26,9 @@
 (defn- create-form-with-fields [form-fields]
   (-> (request :post "/api/forms/create")
       (authenticate "42" "owner")
-      (json-body {:organization "abc"
-                  :title ""
-                  :fields form-fields})
+      (json-body {:form/organization "abc"
+                  :form/title ""
+                  :form/fields form-fields})
       handler
       read-ok-body
       :id))
@@ -282,13 +282,13 @@
         (testing "add licenses"
           (let [application (get-application application-id user-id)]
             (is (= #{1 2} (license-ids-for-application application)))
-           (is (= {:success true} (send-command handler-id
-                                                {:type :application.command/add-licenses
-                                                 :application-id application-id
-                                                 :licenses [license-id]
-                                                 :comment "Please approve these new terms"})))
-           (let [application (get-application application-id user-id)]
-             (is (= #{1 2 5} (license-ids-for-application application))))))
+            (is (= {:success true} (send-command handler-id
+                                                 {:type :application.command/add-licenses
+                                                  :application-id application-id
+                                                  :licenses [license-id]
+                                                  :comment "Please approve these new terms"})))
+            (let [application (get-application application-id user-id)]
+              (is (= #{1 2 5} (license-ids-for-application application))))))
         (testing "applicant accepts the additional licenses"
           (is (= {:success true} (send-command user-id
                                                {:type :application.command/accept-licenses
@@ -300,19 +300,19 @@
           (is (= #{9} (catalogue-item-ids-for-application application)))
           ;; License #5 was added previously by the handler.
           (is (= #{1 2 5} (license-ids-for-application application)))
-         (is (= {:success true} (send-command handler-id
-                                              {:type :application.command/change-resources
-                                               :application-id application-id
-                                               :catalogue-item-ids [9 10]
-                                               :comment "Here are the correct resources"})))
-         (let [application (get-application application-id user-id)]
-           (is (= #{9 10} (catalogue-item-ids-for-application application)))
-           ;; License #4 is added by the catalogue-item #10, whereas
-           ;; the previously added license #5 is dropped from the list.
-           ;;
-           ;; TODO: The previously added licenses should probably be retained
-           ;; in the licenses after changing resources.
-           (is (= #{1 2 4} (license-ids-for-application application))))))
+          (is (= {:success true} (send-command handler-id
+                                               {:type :application.command/change-resources
+                                                :application-id application-id
+                                                :catalogue-item-ids [9 10]
+                                                :comment "Here are the correct resources"})))
+          (let [application (get-application application-id user-id)]
+            (is (= #{9 10} (catalogue-item-ids-for-application application)))
+            ;; License #4 is added by the catalogue-item #10, whereas
+            ;; the previously added license #5 is dropped from the list.
+            ;;
+            ;; TODO: The previously added licenses should probably be retained
+            ;; in the licenses after changing resources.
+            (is (= #{1 2 4} (license-ids-for-application application))))))
 
       (testing "changing resources back as handler"
         (is (= {:success true} (send-command handler-id
@@ -439,15 +439,15 @@
 (deftest test-application-validation
   (let [user-id "alice"
         workflow-id (create-dynamic-workflow)
-        form-id (create-form-with-fields [{:title {:en "req"}
-                                           :type "text"
-                                           :optional false}
-                                          {:title {:en "opt"}
-                                           :type "text"
-                                           :optional true}])
+        form-id (create-form-with-fields [{:field/title {:en "req"}
+                                           :field/type :text
+                                           :field/optional false}
+                                          {:field/title {:en "opt"}
+                                           :field/type :text
+                                           :field/optional true}])
         [req-id opt-id] (->> (form/get-form-template form-id)
-                             :fields
-                             (map :id))
+                             :form/fields
+                             (map :field/id))
         cat-id (create-catalogue-item form-id workflow-id)
         app-id (create-application [cat-id] user-id)]
     (testing "set value of optional field"
@@ -489,9 +489,9 @@
   (let [api-key "42"
         user-id "alice"
         workflow-id (create-dynamic-workflow)
-        form-id (create-form-with-fields [{:title {:en "some attachment"}
-                                           :type "attachment"
-                                           :optional true}])
+        form-id (create-form-with-fields [{:field/title {:en "some attachment"}
+                                           :field/type :attachment
+                                           :field/optional true}])
         cat-id (create-catalogue-item form-id workflow-id)
         app-id (create-application [cat-id] user-id)
         upload-request (fn [file]
diff --git a/test/clj/rems/api/test_forms.clj b/test/clj/rems/api/test_forms.clj
index d177a7f0b..38643e954 100644
--- a/test/clj/rems/api/test_forms.clj
+++ b/test/clj/rems/api/test_forms.clj
@@ -1,8 +1,6 @@
 (ns ^:integration rems.api.test-forms
   (:require [clojure.test :refer :all]
             [rems.api.testing :refer :all]
-            [rems.db.core :as db]
-            [rems.db.form :as form]
             [rems.handler :refer [handler]]
             [ring.mock.request :refer :all])
   (:import (java.util UUID)))
@@ -11,6 +9,12 @@
   :once
   api-fixture)
 
+(defn fixup-field-to-match-command [field]
+  (-> field
+      (dissoc :field/id)
+      ;; XXX: these tests use the JSON API, so keywords are not maintained
+      (update :field/type keyword)))
+
 (deftest forms-api-test
   (let [api-key "42"
         user-id "owner"]
@@ -21,34 +25,48 @@
                      handler
                      assert-response-is-ok
                      read-body)]
-        (is (:id (first data)))))
+        (is (:form/id (first data)))))
+
+    (testing "get one"
+      (let [data (-> (request :get "/api/forms/1")
+                     (authenticate api-key user-id)
+                     handler
+                     assert-response-is-ok
+                     read-body)]
+        (is (:form/id data))))
+
+    (testing "not found"
+      (let [response (-> (request :get "/api/forms/0")
+                         (authenticate api-key user-id)
+                         handler)]
+        (is (= 404 (:status response)))))
 
     (testing "create"
-      (let [command {:organization "abc"
-                     :title (str "form title " (UUID/randomUUID))
-                     :fields [{:title {:en "en title"
-                                       :fi "fi title"}
-                               :optional true
-                               :type "text"
-                               :input-prompt {:en "en prompt"
-                                              :fi "fi prompt"}}]}]
+      (let [command {:form/organization "abc"
+                     :form/title (str "form title " (UUID/randomUUID))
+                     :form/fields [{:field/title {:en "en title"
+                                                  :fi "fi title"}
+                                    :field/optional true
+                                    :field/type :text
+                                    :field/placeholder {:en "en placeholder"
+                                                        :fi "fi placeholder"}}]}]
 
         (testing "invalid create"
           ;; TODO: silence the logging for this expected error
-          (let [command-with-invalid-maxlength (assoc-in command [:fields 0 :maxlength] -1)
+          (let [command-with-invalid-max-length (assoc-in command [:form/fields 0 :field/max-length] -1)
                 response (-> (request :post "/api/forms/create")
                              (authenticate api-key user-id)
-                             (json-body command-with-invalid-maxlength)
+                             (json-body command-with-invalid-max-length)
                              handler)]
             (is (= 400 (:status response))
-                "can't send negative maxlength")))
+                "can't send negative max length")))
 
         (testing "invalid create: field too long"
-          (let [command-with-long-prompt (assoc-in command [:fields 0 :input-prompt :en]
-                                                   (apply str (repeat 10000 "x")))
+          (let [command-with-long-placeholder (assoc-in command [:form/fields 0 :field/placeholder :en]
+                                                        (apply str (repeat 10000 "x")))
                 response (-> (request :post "/api/forms/create")
                              (authenticate api-key user-id)
-                             (json-body command-with-long-prompt)
+                             (json-body command-with-long-placeholder)
                              handler)]
             (is (= 500 (:status response)))))
 
@@ -66,50 +84,50 @@
                                       handler
                                       read-ok-body)]
                 (testing "result matches input"
-                  (is (= (select-keys command [:title :organization])
-                         (select-keys form-template [:title :organization])))
-                  (is (= (:fields command)
-                         (mapv #(dissoc % :id) (:fields form-template)))))))))))))
+                  (is (= (select-keys command [:form/organization :form/title])
+                         (select-keys form-template [:form/organization :form/title])))
+                  (is (= (:form/fields command)
+                         (mapv fixup-field-to-match-command (:form/fields form-template)))))))))))))
 
 (deftest forms-api-all-field-types-test
   (let [api-key "42"
         user-id "owner"
-        ;;"attachment" "date" "description" "label" "multiselect" "option" "text" "texta"
         localized {:en "en" :fi "fi"}
-        form-spec {:organization "abc" :title "all field types test"
-                   :fields [{:type "text"
-                             :title localized
-                             :optional false}
-                            {:type "texta"
-                             :title localized
-                             :optional true
-                             :maxlength 300
-                             :input-prompt localized}
-                            {:type "description"
-                             :title localized
-                             :optional false}
-                            {:type "option"
-                             :title localized
-                             :optional true
-                             :options [{:key "a" :label localized}
-                                       {:key "b" :label localized}
-                                       {:key "c" :label localized}]}
-                            {:type "multiselect"
-                             :title localized
-                             :optional false
-                             :options [{:key "a" :label localized}
-                                       {:key "b" :label localized}
-                                       {:key "c" :label localized}
-                                       {:key "d" :label localized}]}
-                            {:type "label"
-                             :title localized
-                             :optional true}
-                            {:type "date"
-                             :title localized
-                             :optional true}
-                            {:type "attachment"
-                             :title localized
-                             :optional false}]}]
+        form-spec {:form/organization "abc"
+                   :form/title "all field types test"
+                   :form/fields [{:field/type :text
+                                  :field/title localized
+                                  :field/optional false}
+                                 {:field/type :texta
+                                  :field/title localized
+                                  :field/optional true
+                                  :field/max-length 300
+                                  :field/placeholder localized}
+                                 {:field/type :description
+                                  :field/title localized
+                                  :field/optional false}
+                                 {:field/type :option
+                                  :field/title localized
+                                  :field/optional true
+                                  :field/options [{:key "a" :label localized}
+                                                  {:key "b" :label localized}
+                                                  {:key "c" :label localized}]}
+                                 {:field/type :multiselect
+                                  :field/title localized
+                                  :field/optional false
+                                  :field/options [{:key "a" :label localized}
+                                                  {:key "b" :label localized}
+                                                  {:key "c" :label localized}
+                                                  {:key "d" :label localized}]}
+                                 {:field/type :label
+                                  :field/title localized
+                                  :field/optional true}
+                                 {:field/type :date
+                                  :field/title localized
+                                  :field/optional true}
+                                 {:field/type :attachment
+                                  :field/title localized
+                                  :field/optional false}]}]
     (testing "creating"
       (let [form-id (-> (request :post "/api/forms/create")
                         (authenticate api-key user-id)
@@ -123,18 +141,19 @@
                          (authenticate api-key user-id)
                          handler
                          read-ok-body)]
-            (is (= (select-keys form-spec [:organization :title])
-                   (select-keys form [:organization :title])))
-            (is (= (:fields form-spec)
-                   (mapv #(dissoc % :id) (:fields form))))))))))
+            (is (= (select-keys form-spec [:form/organization :form/title])
+                   (select-keys form [:form/organization :form/title])))
+            (is (= (:form/fields form-spec)
+                   (mapv fixup-field-to-match-command (:form/fields form))))))))))
 
 (deftest form-editable-test
   (let [api-key "42"
         user-id "owner"
         form-id (-> (request :post "/api/forms/create")
                     (authenticate api-key user-id)
-                    (json-body {:organization "abc" :title "form editable test"
-                                :fields []})
+                    (json-body {:form/organization "abc"
+                                :form/title "form editable test"
+                                :form/fields []})
                     handler
                     read-ok-body
                     :id)]
@@ -163,8 +182,9 @@
         user-id "owner"
         form-id (-> (request :post "/api/forms/create")
                     (authenticate api-key user-id)
-                    (json-body {:organization "abc" :title "form edit test"
-                                :fields []})
+                    (json-body {:form/organization "abc"
+                                :form/title "form edit test"
+                                :form/fields []})
                     handler
                     read-ok-body
                     :id)]
@@ -173,27 +193,29 @@
                      (authenticate api-key user-id)
                      handler
                      read-ok-body)]
-        (is (= (form :organization) "abc")))
-     (let [response (-> (request :put (str "/api/forms/" form-id "/edit"))
-                        (authenticate api-key user-id)
-                        (json-body {:organization "def" :title "form edit test"
-                                    :fields []})
-                        handler
-                        read-ok-body)]
-       (testing "form content after editing"
-         (let [form (-> (request :get (str "/api/forms/" form-id))
-                        (authenticate api-key user-id)
-                        handler
-                        read-ok-body)]
-           (is (= (form :organization) "def"))))))))
+        (is (= (:form/organization form) "abc")))
+      (let [response (-> (request :put (str "/api/forms/" form-id "/edit"))
+                         (authenticate api-key user-id)
+                         (json-body {:form/organization "def"
+                                     :form/title "form edit test"
+                                     :form/fields []})
+                         handler
+                         read-ok-body)]
+        (testing "form content after editing"
+          (let [form (-> (request :get (str "/api/forms/" form-id))
+                         (authenticate api-key user-id)
+                         handler
+                         read-ok-body)]
+            (is (= (:form/organization form) "def"))))))))
 
 (deftest form-update-test
   (let [api-key "42"
         user-id "owner"
         form-id (-> (request :post "/api/forms/create")
                     (authenticate api-key user-id)
-                    (json-body {:organization "abc" :title "form update test"
-                                :fields []})
+                    (json-body {:form/organization "abc"
+                                :form/title "form update test"
+                                :form/fields []})
                     handler
                     read-ok-body
                     :id)]
@@ -233,18 +255,18 @@
   (let [api-key "42"
         user-id "owner"]
     (testing "create"
-      (let [command {:organization "abc"
-                     :title (str "form title " (UUID/randomUUID))
-                     :fields [{:title {:en "en title"
-                                       :fi "fi title"}
-                               :optional true
-                               :type "option"
-                               :options [{:key "yes"
-                                          :label {:en "Yes"
-                                                  :fi "Kyllä"}}
-                                         {:key "no"
-                                          :label {:en "No"
-                                                  :fi "Ei"}}]}]}
+      (let [command {:form/organization "abc"
+                     :form/title (str "form title " (UUID/randomUUID))
+                     :form/fields [{:field/title {:en "en title"
+                                                  :fi "fi title"}
+                                    :field/optional true
+                                    :field/type :option
+                                    :field/options [{:key "yes"
+                                                     :label {:en "Yes"
+                                                             :fi "Kyllä"}}
+                                                    {:key "no"
+                                                     :label {:en "No"
+                                                             :fi "Ei"}}]}]}
             id (-> (request :post "/api/forms/create")
                    (authenticate api-key user-id)
                    (json-body command)
@@ -257,8 +279,8 @@
                          (authenticate api-key user-id)
                          handler
                          read-ok-body)]
-            (is (= (:fields command)
-                   (mapv #(dissoc % :id) (:fields form))))))))))
+            (is (= (:form/fields command)
+                   (mapv fixup-field-to-match-command (:form/fields form))))))))))
 
 (deftest forms-api-filtering-test
   (let [unfiltered (-> (request :get "/api/forms" {:archived true})
@@ -287,9 +309,9 @@
         (is (= "unauthorized" body))))
     (testing "create"
       (let [response (-> (request :post "/api/forms/create")
-                         (json-body {:organization "abc"
-                                     :title "the title"
-                                     :fields []})
+                         (json-body {:form/organization "abc"
+                                     :form/title "the title"
+                                     :form/fields []})
                          handler)]
         (is (response-is-unauthorized? response))
         (is (= "Invalid anti-forgery token" (read-body response))))))
@@ -305,9 +327,9 @@
     (testing "create"
       (let [response (-> (request :post "/api/forms/create")
                          (authenticate "42" "alice")
-                         (json-body {:organization "abc"
-                                     :title "the title"
-                                     :fields []})
+                         (json-body {:form/organization "abc"
+                                     :form/title "the title"
+                                     :form/fields []})
                          handler)]
         (is (response-is-forbidden? response))
         (is (= "forbidden" (read-body response)))))))
diff --git a/test/clj/rems/application/test_commands.clj b/test/clj/rems/application/test_commands.clj
index 2724007f3..c99a74777 100644
--- a/test/clj/rems/application/test_commands.clj
+++ b/test/clj/rems/application/test_commands.clj
@@ -1,6 +1,5 @@
 (ns rems.application.test-commands
-  (:require [clojure.string :as str]
-            [clojure.test :refer :all]
+  (:require [clojure.test :refer :all]
             [rems.application.commands :as commands]
             [rems.application.events :as events]
             [rems.application.model :as model]
@@ -26,11 +25,7 @@
                                     :workflow/id 1
                                     :workflow/type :workflow/dynamic})
 (def ^:private dummy-workflows {1 {:workflow {:handlers [handler-user-id]}}})
-
-(def ^:private dummy-forms {1 {:items [{:id 41
-                                        :optional false}
-                                       {:id 42
-                                        :optional false}]}})
+(def ^:private dummy-forms {1 {}})
 
 (def ^:private injections
   {:get-workflow dummy-workflows
diff --git a/test/clj/rems/application/test_model.clj b/test/clj/rems/application/test_model.clj
index 2327f5976..584dfe33d 100644
--- a/test/clj/rems/application/test_model.clj
+++ b/test/clj/rems/application/test_model.clj
@@ -1,32 +1,37 @@
 (ns rems.application.test-model
   (:require [clojure.test :refer :all]
+            [rems.api.schema :as schema]
             [rems.application.events :as events]
             [rems.application.model :as model]
             [rems.common-util :refer [deep-merge]]
-            [rems.permissions :as permissions])
+            [rems.permissions :as permissions]
+            [schema.core :as s])
   (:import [java.util UUID]
            [org.joda.time DateTime]))
 
 (def ^:private get-form-template
-  {40 {:id 40
-       :organization "org"
-       :title "form title"
-       :fields [{:id 41
-                 :title {:en "en title" :fi "fi title"}
-                 :input-prompt {:en "en placeholder" :fi "fi placeholder"}
-                 :optional false
-                 :options []
-                 :maxlength 100
-                 :type "description"}
-                {:id 42
-                 :title {:en "en title" :fi "fi title"}
-                 :input-prompt {:en "en placeholder" :fi "fi placeholder"}
-                 :optional false
-                 :options []
-                 :maxlength 100
-                 :type "text"}]
+  {40 {:form/id 40
+       :form/organization "org"
+       :form/title "form title"
+       :form/fields [{:field/id 41
+                      :field/title {:en "en title" :fi "fi title"}
+                      :field/placeholder {:en "en placeholder" :fi "fi placeholder"}
+                      :field/optional false
+                      :field/options []
+                      :field/max-length 100
+                      :field/type :description}
+                     {:field/id 42
+                      :field/title {:en "en title" :fi "fi title"}
+                      :field/placeholder {:en "en placeholder" :fi "fi placeholder"}
+                      :field/optional false
+                      :field/options []
+                      :field/max-length 100
+                      :field/type :text}]
        :start (DateTime. 100)
-       :end nil}})
+       :end nil
+       :enabled true
+       :archived false
+       :expired false}})
 
 (def ^:private get-catalogue-item
   {10 {:id 10
@@ -45,8 +50,7 @@
        :end nil
        :enabled true
        :archived false
-       :expired false
-       :state "enabled"}
+       :expired false}
    20 {:id 20
        :resource-id 21
        :resid "urn:21"
@@ -63,8 +67,7 @@
        :end nil
        :enabled true
        :archived false
-       :expired false
-       :state "enabled"}
+       :expired false}
    30 {:id 30
        :resource-id 31
        :resid "urn:31"
@@ -81,8 +84,7 @@
        :end nil
        :enabled true
        :archived false
-       :expired false
-       :state "enabled"}})
+       :expired false}})
 
 (def ^:private get-license
   {30 {:id 30
@@ -190,6 +192,15 @@
 (defn ^:private get-attachments-for-application [id]
   [])
 
+(deftest test-dummies-schema
+  (doseq [[description schema dummies] [["form template" schema/FormTemplate get-form-template]
+                                        ["catalogue item" schema/CatalogueItem get-catalogue-item]
+                                        ["license" schema/License get-license]
+                                        ["workflow" schema/WorkflowDB get-workflow]]]
+    (doseq [[id dummy] dummies]
+      (testing (str description " " id)
+        (is (s/validate schema dummy))))))
+
 (deftest test-application-view
   (let [injections {:get-form-template get-form-template
                     :get-catalogue-item get-catalogue-item
diff --git a/test/clj/rems/db/test_applications.clj b/test/clj/rems/db/test_applications.clj
index 09c7187b5..470e6863c 100644
--- a/test/clj/rems/db/test_applications.clj
+++ b/test/clj/rems/db/test_applications.clj
@@ -63,9 +63,9 @@
                                                :handlers []
                                                :user-id "owner"}))
         _ (assert wf-id)
-        form-id (:id (form/create-form! "owner" {:organization "abc"
-                                                 :title ""
-                                                 :items []}))
+        form-id (:id (form/create-form! "owner" {:form/organization "abc"
+                                                 :form/title ""
+                                                 :form/fields []}))
         _ (assert form-id)
         res-id (:id (resource/create-resource! {:resid "res1"
                                                 :organization "abc"
@@ -139,9 +139,9 @@
                                                         :actor "alice"}))))
 
     (testing "error: catalogue items with different forms"
-      (let [form-id2 (:id (form/create-form! "owner" {:organization "abc"
-                                                      :title ""
-                                                      :items []}))
+      (let [form-id2 (:id (form/create-form! "owner" {:form/organization "abc"
+                                                      :form/title ""
+                                                      :form/fields []}))
             _ (assert form-id2)
             res-id2 (:id (resource/create-resource! {:resid "res2+"
                                                      :organization "abc"
diff --git a/test/clj/rems/db/test_transactions.clj b/test/clj/rems/db/test_transactions.clj
index e873a202e..99d6e6017 100644
--- a/test/clj/rems/db/test_transactions.clj
+++ b/test/clj/rems/db/test_transactions.clj
@@ -33,9 +33,9 @@
                                                      :type :dynamic
                                                      :handlers []}))
         form-id (:id (form/create-form! user-id
-                                        {:organization ""
-                                         :title ""
-                                         :fields []}))
+                                        {:form/organization ""
+                                         :form/title ""
+                                         :form/fields []}))
         res-id (:id (resource/create-resource! {:resid (str "urn:uuid:" (UUID/randomUUID))
                                                 :organization ""
                                                 :licenses []}
diff --git a/test/cljs/rems/administration/test_create_form.cljs b/test/cljs/rems/administration/test_create_form.cljs
index eb57bea49..023077d25 100644
--- a/test/cljs/rems/administration/test_create_form.cljs
+++ b/test/cljs/rems/administration/test_create_form.cljs
@@ -14,27 +14,27 @@
   (let [form (rf/subscribe [::f/form])]
     (testing "adds fields"
       (reset-form)
-      (is (= {:fields []}
+      (is (= {:form/fields []}
              @form)
           "before")
 
       (rf/dispatch-sync [::f/add-form-field])
 
-      (is (= {:fields [{:type "text"}]}
+      (is (= {:form/fields [{:field/type :text}]}
              @form)
           "after"))
 
     (testing "adds fields to the end"
       (reset-form)
       (rf/dispatch-sync [::f/add-form-field])
-      (rf/dispatch-sync [::f/set-form-field [:fields 0 :foo] "old field"])
-      (is (= {:fields [{:type "text" :foo "old field"}]}
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 0 :foo] "old field"])
+      (is (= {:form/fields [{:field/type :text :foo "old field"}]}
              @form)
           "before")
 
       (rf/dispatch-sync [::f/add-form-field])
 
-      (is (= {:fields [{:type "text" :foo "old field"} {:type "text"}]}
+      (is (= {:form/fields [{:field/type :text :foo "old field"} {:field/type :text}]}
              @form)
           "after"))))
 
@@ -43,13 +43,13 @@
     (testing "removes fields"
       (reset-form)
       (rf/dispatch-sync [::f/add-form-field])
-      (is (= {:fields [{:type "text"}]}
+      (is (= {:form/fields [{:field/type :text}]}
              @form)
           "before")
 
       (rf/dispatch-sync [::f/remove-form-field 0])
 
-      (is (= {:fields []}
+      (is (= {:form/fields []}
              @form)
           "after"))
 
@@ -58,19 +58,19 @@
       (rf/dispatch-sync [::f/add-form-field])
       (rf/dispatch-sync [::f/add-form-field])
       (rf/dispatch-sync [::f/add-form-field])
-      (rf/dispatch-sync [::f/set-form-field [:fields 0 :foo] "field 0"])
-      (rf/dispatch-sync [::f/set-form-field [:fields 1 :foo] "field 1"])
-      (rf/dispatch-sync [::f/set-form-field [:fields 2 :foo] "field 2"])
-      (is (= {:fields [{:type "text" :foo "field 0"}
-                      {:type "text" :foo "field 1"}
-                      {:type "text" :foo "field 2"}]}
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 0 :foo] "field 0"])
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 1 :foo] "field 1"])
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 2 :foo] "field 2"])
+      (is (= {:form/fields [{:field/type :text :foo "field 0"}
+                            {:field/type :text :foo "field 1"}
+                            {:field/type :text :foo "field 2"}]}
              @form)
           "before")
 
       (rf/dispatch-sync [::f/remove-form-field 1])
 
-      (is (= {:fields [{:type "text" :foo "field 0"}
-                      {:type "text" :foo "field 2"}]}
+      (is (= {:form/fields [{:field/type :text :foo "field 0"}
+                            {:field/type :text :foo "field 2"}]}
              @form)
           "after"))))
 
@@ -81,37 +81,37 @@
       (rf/dispatch-sync [::f/add-form-field])
       (rf/dispatch-sync [::f/add-form-field])
       (rf/dispatch-sync [::f/add-form-field])
-      (rf/dispatch-sync [::f/set-form-field [:fields 0 :foo] "field 0"])
-      (rf/dispatch-sync [::f/set-form-field [:fields 1 :foo] "field 1"])
-      (rf/dispatch-sync [::f/set-form-field [:fields 2 :foo] "field X"])
-      (is (= {:fields [{:type "text" :foo "field 0"}
-                      {:type "text" :foo "field 1"}
-                      {:type "text" :foo "field X"}]}
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 0 :foo] "field 0"])
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 1 :foo] "field 1"])
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 2 :foo] "field X"])
+      (is (= {:form/fields [{:field/type :text :foo "field 0"}
+                            {:field/type :text :foo "field 1"}
+                            {:field/type :text :foo "field X"}]}
              @form)
           "before")
 
       (rf/dispatch-sync [::f/move-form-field-up 2])
 
-      (is (= {:fields [{:type "text" :foo "field 0"}
-                      {:type "text" :foo "field X"}
-                      {:type "text" :foo "field 1"}]}
+      (is (= {:form/fields [{:field/type :text :foo "field 0"}
+                            {:field/type :text :foo "field X"}
+                            {:field/type :text :foo "field 1"}]}
              @form)
           "after move 1")
 
       (rf/dispatch-sync [::f/move-form-field-up 1])
 
-      (is (= {:fields [{:type "text" :foo "field X"}
-                      {:type "text" :foo "field 0"}
-                      {:type "text" :foo "field 1"}]}
+      (is (= {:form/fields [{:field/type :text :foo "field X"}
+                            {:field/type :text :foo "field 0"}
+                            {:field/type :text :foo "field 1"}]}
              @form)
           "after move 2")
 
       (testing "unless already first"
         (rf/dispatch-sync [::f/move-form-field-up 0])
 
-        (is (= {:fields [{:type "text" :foo "field X"}
-                        {:type "text" :foo "field 0"}
-                        {:type "text" :foo "field 1"}]}
+        (is (= {:form/fields [{:field/type :text :foo "field X"}
+                              {:field/type :text :foo "field 0"}
+                              {:field/type :text :foo "field 1"}]}
                @form)
             "after move 3")))))
 
@@ -122,247 +122,247 @@
       (rf/dispatch-sync [::f/add-form-field])
       (rf/dispatch-sync [::f/add-form-field])
       (rf/dispatch-sync [::f/add-form-field])
-      (rf/dispatch-sync [::f/set-form-field [:fields 0 :foo] "field X"])
-      (rf/dispatch-sync [::f/set-form-field [:fields 1 :foo] "field 1"])
-      (rf/dispatch-sync [::f/set-form-field [:fields 2 :foo] "field 2"])
-      (is (= {:fields [{:type "text" :foo "field X"}
-                      {:type "text" :foo "field 1"}
-                      {:type "text" :foo "field 2"}]}
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 0 :foo] "field X"])
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 1 :foo] "field 1"])
+      (rf/dispatch-sync [::f/set-form-field [:form/fields 2 :foo] "field 2"])
+      (is (= {:form/fields [{:field/type :text :foo "field X"}
+                            {:field/type :text :foo "field 1"}
+                            {:field/type :text :foo "field 2"}]}
              @form)
           "before")
 
       (rf/dispatch-sync [::f/move-form-field-down 0])
 
-      (is (= {:fields [{:type "text" :foo "field 1"}
-                      {:type "text" :foo "field X"}
-                      {:type "text" :foo "field 2"}]}
+      (is (= {:form/fields [{:field/type :text :foo "field 1"}
+                            {:field/type :text :foo "field X"}
+                            {:field/type :text :foo "field 2"}]}
              @form)
           "after move 1")
 
       (rf/dispatch-sync [::f/move-form-field-down 1])
 
-      (is (= {:fields [{:type "text" :foo "field 1"}
-                      {:type "text" :foo "field 2"}
-                      {:type "text" :foo "field X"}]}
+      (is (= {:form/fields [{:field/type :text :foo "field 1"}
+                            {:field/type :text :foo "field 2"}
+                            {:field/type :text :foo "field X"}]}
              @form)
           "after move 2")
 
       (testing "unless already last"
         (rf/dispatch-sync [::f/move-form-field-down 2])
 
-        (is (= {:fields [{:type "text" :foo "field 1"}
-                        {:type "text" :foo "field 2"}
-                        {:type "text" :foo "field X"}]}
+        (is (= {:form/fields [{:field/type :text :foo "field 1"}
+                              {:field/type :text :foo "field 2"}
+                              {:field/type :text :foo "field X"}]}
                @form)
             "after move 3")))))
 
 (deftest build-request-test
-  (let [form {:organization "abc"
-              :title "the title"
-              :fields [{:title {:en "en title"
-                                :fi "fi title"}
-                       :optional true
-                       :type "text"
-                       :maxlength "12"
-                       :input-prompt {:en "en prompt"
-                                      :fi "fi prompt"}}]}
+  (let [form {:form/organization "abc"
+              :form/title "the title"
+              :form/fields [{:field/title {:en "en title"
+                                           :fi "fi title"}
+                             :field/optional true
+                             :field/type :text
+                             :field/max-length "12"
+                             :field/placeholder {:en "en placeholder"
+                                                 :fi "fi placeholder"}}]}
         languages [:en :fi]]
 
     (testing "basic form"
-      (is (= {:organization "abc"
-              :title "the title"
-              :fields [{:title {:en "en title"
-                                :fi "fi title"}
-                        :optional true
-                        :type "text"
-                        :maxlength 12
-                        :input-prompt {:en "en prompt"
-                                       :fi "fi prompt"}}]}
+      (is (= {:form/organization "abc"
+              :form/title "the title"
+              :form/fields [{:field/title {:en "en title"
+                                           :fi "fi title"}
+                             :field/optional true
+                             :field/type :text
+                             :field/max-length 12
+                             :field/placeholder {:en "en placeholder"
+                                                 :fi "fi placeholder"}}]}
              (build-request form languages))))
 
     (testing "zero fields"
-      (is (= {:organization "abc"
-              :title "the title"
-              :fields []}
-             (build-request (assoc-in form [:fields] []) languages))))
+      (is (= {:form/organization "abc"
+              :form/title "the title"
+              :form/fields []}
+             (build-request (assoc-in form [:form/fields] []) languages))))
 
     (testing "date fields"
-      (let [form (assoc-in form [:fields 0 :type] "date")]
-        (is (= {:organization "abc"
-                :title "the title"
-                :fields [{:title {:en "en title"
-                                  :fi "fi title"}
-                          :optional true
-                          :type "date"}]}
+      (let [form (assoc-in form [:form/fields 0 :field/type] :date)]
+        (is (= {:form/organization "abc"
+                :form/title "the title"
+                :form/fields [{:field/title {:en "en title"
+                                             :fi "fi title"}
+                               :field/optional true
+                               :field/type :date}]}
                (build-request form languages)))))
 
     (testing "missing optional implies false"
-      (is (false? (getx-in (build-request (assoc-in form [:fields 0 :optional] nil) languages)
-                           [:fields 0 :optional]))))
+      (is (false? (getx-in (build-request (assoc-in form [:form/fields 0 :field/optional] nil) languages)
+                           [:form/fields 0 :field/optional]))))
 
-    (testing "input prompt is optional"
+    (testing "placeholder is optional"
       (is (= {:en "" :fi ""}
-             (getx-in (build-request (assoc-in form [:fields 0 :input-prompt] nil) languages)
-                      [:fields 0 :input-prompt])
-             (getx-in (build-request (assoc-in form [:fields 0 :input-prompt] {:en ""}) languages)
-                      [:fields 0 :input-prompt])
-             (getx-in (build-request (assoc-in form [:fields 0 :input-prompt] {:en "" :fi ""}) languages)
-                      [:fields 0 :input-prompt]))))
-
-    (testing "maxlength is optional"
-      (is (nil? (getx-in (build-request (assoc-in form [:fields 0 :maxlength] "") languages)
-                         [:fields 0 :maxlength])))
-      (is (nil? (getx-in (build-request (assoc-in form [:fields 0 :maxlength] nil) languages)
-                         [:fields 0 :maxlength]))))
+             (getx-in (build-request (assoc-in form [:form/fields 0 :field/placeholder] nil) languages)
+                      [:form/fields 0 :field/placeholder])
+             (getx-in (build-request (assoc-in form [:form/fields 0 :field/placeholder] {:en ""}) languages)
+                      [:form/fields 0 :field/placeholder])
+             (getx-in (build-request (assoc-in form [:form/fields 0 :field/placeholder] {:en "" :fi ""}) languages)
+                      [:form/fields 0 :field/placeholder]))))
+
+    (testing "max length is optional"
+      (is (nil? (getx-in (build-request (assoc-in form [:form/fields 0 :field/max-length] "") languages)
+                         [:form/fields 0 :field/max-length])))
+      (is (nil? (getx-in (build-request (assoc-in form [:form/fields 0 :field/max-length] nil) languages)
+                         [:form/fields 0 :field/max-length]))))
 
     (testing "option fields"
       (let [form (-> form
-                     (assoc-in [:fields 0 :type] "option")
-                     (assoc-in [:fields 0 :options] [{:key "yes"
-                                                      :label {:en "en yes"
-                                                              :fi "fi yes"}}
-                                                     {:key "no"
-                                                      :label {:en "en no"
-                                                              :fi "fi no"}}]))]
-        (is (= {:organization "abc"
-                :title "the title"
-                :fields [{:title {:en "en title"
-                                  :fi "fi title"}
-                          :optional true
-                          :type "option"
-                          :options [{:key "yes"
-                                     :label {:en "en yes"
-                                             :fi "fi yes"}}
-                                    {:key "no"
-                                     :label {:en "en no"
-                                             :fi "fi no"}}]}]}
+                     (assoc-in [:form/fields 0 :field/type] :option)
+                     (assoc-in [:form/fields 0 :field/options] [{:key "yes"
+                                                                 :label {:en "en yes"
+                                                                         :fi "fi yes"}}
+                                                                {:key "no"
+                                                                 :label {:en "en no"
+                                                                         :fi "fi no"}}]))]
+        (is (= {:form/organization "abc"
+                :form/title "the title"
+                :form/fields [{:field/title {:en "en title"
+                                             :fi "fi title"}
+                               :field/optional true
+                               :field/type :option
+                               :field/options [{:key "yes"
+                                                :label {:en "en yes"
+                                                        :fi "fi yes"}}
+                                               {:key "no"
+                                                :label {:en "en no"
+                                                        :fi "fi no"}}]}]}
                (build-request form languages)))))
 
     (testing "multiselect fields"
       (let [form (-> form
-                     (assoc-in [:fields 0 :type] "multiselect")
-                     (assoc-in [:fields 0 :options] [{:key "egg"
-                                                      :label {:en "Egg"
-                                                              :fi "Munaa"}}
-                                                     {:key "bacon"
-                                                      :label {:en "Bacon"
-                                                              :fi "Pekonia"}}]))]
-        (is (= {:organization "abc"
-                :title "the title"
-                :fields [{:title {:en "en title"
-                                  :fi "fi title"}
-                          :optional true
-                          :type "multiselect"
-                          :options [{:key "egg"
-                                     :label {:en "Egg"
-                                             :fi "Munaa"}}
-                                    {:key "bacon"
-                                     :label {:en "Bacon"
-                                             :fi "Pekonia"}}]}]}
+                     (assoc-in [:form/fields 0 :field/type] :multiselect)
+                     (assoc-in [:form/fields 0 :field/options] [{:key "egg"
+                                                                 :label {:en "Egg"
+                                                                         :fi "Munaa"}}
+                                                                {:key "bacon"
+                                                                 :label {:en "Bacon"
+                                                                         :fi "Pekonia"}}]))]
+        (is (= {:form/organization "abc"
+                :form/title "the title"
+                :form/fields [{:field/title {:en "en title"
+                                             :fi "fi title"}
+                               :field/optional true
+                               :field/type :multiselect
+                               :field/options [{:key "egg"
+                                                :label {:en "Egg"
+                                                        :fi "Munaa"}}
+                                               {:key "bacon"
+                                                :label {:en "Bacon"
+                                                        :fi "Pekonia"}}]}]}
                (build-request form languages)))))))
 
 (deftest validate-form-test
-  (let [form {:organization "abc"
-              :title "the title"
-              :fields [{:title {:en "en title"
-                                :fi "fi title"}
-                       :optional true
-                       :type "text"
-                       :maxlength "12"
-                       :input-prompt {:en "en prompt"
-                                      :fi "fi prompt"}}]}
+  (let [form {:form/organization "abc"
+              :form/title "the title"
+              :form/fields [{:field/title {:en "en title"
+                                           :fi "fi title"}
+                             :field/optional true
+                             :field/type :text
+                             :field/max-length "12"
+                             :field/placeholder {:en "en placeholder"
+                                                 :fi "fi placeholder"}}]}
         languages [:en :fi]]
 
     (testing "valid form"
       (is (empty? (validate-form form languages))))
 
     (testing "missing organization"
-      (is (= (:organization (validate-form (assoc-in form [:organization] "") languages))
+      (is (= (:form/organization (validate-form (assoc-in form [:form/organization] "") languages))
              :t.form.validation/required)))
 
     (testing "missing title"
-      (is (= (:title (validate-form (assoc-in form [:title] "") languages))
+      (is (= (:form/title (validate-form (assoc-in form [:form/title] "") languages))
              :t.form.validation/required)))
 
     (testing "zero fields is ok"
-      (is (empty? (validate-form (assoc-in form [:fields] []) languages))))
+      (is (empty? (validate-form (assoc-in form [:form/fields] []) languages))))
 
     (testing "missing field title"
-      (let [nil-title (validate-form (assoc-in form [:fields 0 :title] nil) languages)]
-        (is (= (get-in (validate-form (assoc-in form [:fields 0 :title :en] "") languages)
-                       [:fields 0 :title :en])
-               (get-in (validate-form (update-in form [:fields 0 :title] dissoc :en) languages)
-                       [:fields 0 :title :en])
-               (get-in nil-title [:fields 0 :title :en])
-               (get-in nil-title [:fields 0 :title :fi])
+      (let [nil-title (validate-form (assoc-in form [:form/fields 0 :field/title] nil) languages)]
+        (is (= (get-in (validate-form (assoc-in form [:form/fields 0 :field/title :en] "") languages)
+                       [:form/fields 0 :field/title :en])
+               (get-in (validate-form (update-in form [:form/fields 0 :field/title] dissoc :en) languages)
+                       [:form/fields 0 :field/title :en])
+               (get-in nil-title [:form/fields 0 :field/title :en])
+               (get-in nil-title [:form/fields 0 :field/title :fi])
                :t.form.validation/required))))
 
     (testing "missing field type"
-      (is (get-in (validate-form (assoc-in form [:fields 0 :type] nil) language)
-                  [:fields 0 :type])
+      (is (get-in (validate-form (assoc-in form [:form/fields 0 :field/type] nil) languages)
+                  [:form/fields 0 :field/type])
           :t.form.validation/required))
 
-    (testing "if you use input prompt, you must fill in all the languages"
-      (is (= (get-in (validate-form (assoc-in form [:fields 0 :input-prompt] {:en "en prompt" :fi ""}) languages)
-                     [:fields 0 :input-prompt :fi])
-             (get-in (validate-form (assoc-in form [:fields 0 :input-prompt] {:en "en prompt"}) languages)
-                     [:fields 0 :input-prompt :fi])
+    (testing "if you use a placeholder, you must fill in all the languages"
+      (is (= (get-in (validate-form (assoc-in form [:form/fields 0 :field/placeholder] {:en "en placeholder" :fi ""}) languages)
+                     [:form/fields 0 :field/placeholder :fi])
+             (get-in (validate-form (assoc-in form [:form/fields 0 :field/placeholder] {:en "en placeholder"}) languages)
+                     [:form/fields 0 :field/placeholder :fi])
              :t.form.validation/required)))
 
     (testing "option fields"
       (let [form (-> form
-                     (assoc-in [:fields 0 :type] "option")
-                     (assoc-in [:fields 0 :options] [{:key "yes"
-                                                      :label {:en "en yes"
-                                                              :fi "fi yes"}}
-                                                     {:key "no"
-                                                      :label {:en "en no"
-                                                              :fi "fi no"}}]))]
+                     (assoc-in [:form/fields 0 :field/type] :option)
+                     (assoc-in [:form/fields 0 :field/options] [{:key "yes"
+                                                                 :label {:en "en yes"
+                                                                         :fi "fi yes"}}
+                                                                {:key "no"
+                                                                 :label {:en "en no"
+                                                                         :fi "fi no"}}]))]
         (testing "valid form"
           (is (empty? (validate-form form languages))))
 
         (testing "missing option key"
-          (is (= (get-in (validate-form (assoc-in form [:fields 0 :options 0 :key] "") languages)
-                         [:fields 0 :options 0 :key])
-                 (get-in (validate-form (assoc-in form [:fields 0 :options 0 :key] nil) languages)
-                         [:fields 0 :options 0 :key])
+          (is (= (get-in (validate-form (assoc-in form [:form/fields 0 :field/options 0 :key] "") languages)
+                         [:form/fields 0 :field/options 0 :key])
+                 (get-in (validate-form (assoc-in form [:form/fields 0 :field/options 0 :key] nil) languages)
+                         [:form/fields 0 :field/options 0 :key])
                  :t.form.validation/required)))
 
         (testing "missing option label"
-          (let [empty-label (validate-form (assoc-in form [:fields 0 :options 0 :label] {:en "" :fi ""}) languages)
-                nil-label (validate-form (assoc-in form [:fields 0 :options 0 :label] nil) languages)]
-            (is (= (get-in empty-label [:fields 0 :options 0 :label :en])
-                   (get-in empty-label [:fields 0 :options 0 :label :fi])
-                   (get-in nil-label [:fields 0 :options 0 :label :en])
-                   (get-in nil-label [:fields 0 :options 0 :label :fi])
+          (let [empty-label (validate-form (assoc-in form [:form/fields 0 :field/options 0 :label] {:en "" :fi ""}) languages)
+                nil-label (validate-form (assoc-in form [:form/fields 0 :field/options 0 :label] nil) languages)]
+            (is (= (get-in empty-label [:form/fields 0 :field/options 0 :label :en])
+                   (get-in empty-label [:form/fields 0 :field/options 0 :label :fi])
+                   (get-in nil-label [:form/fields 0 :field/options 0 :label :en])
+                   (get-in nil-label [:form/fields 0 :field/options 0 :label :fi])
                    :t.form.validation/required))))))
 
     (testing "multiselect fields"
       (let [form (-> form
-                     (assoc-in [:fields 0 :type] "multiselect")
-                     (assoc-in [:fields 0 :options] [{:key "egg"
-                                                      :label {:en "Egg"
-                                                              :fi "Munaa"}}
-                                                     {:key "bacon"
-                                                      :label {:en "Bacon"
-                                                              :fi "Pekonia"}}]))]
+                     (assoc-in [:form/fields 0 :field/type] :multiselect)
+                     (assoc-in [:form/fields 0 :field/options] [{:key "egg"
+                                                                 :label {:en "Egg"
+                                                                         :fi "Munaa"}}
+                                                                {:key "bacon"
+                                                                 :label {:en "Bacon"
+                                                                         :fi "Pekonia"}}]))]
         (testing "valid form"
           (is (empty? (validate-form form languages))))
 
         (testing "missing option key"
-          (is (= (get-in (validate-form (assoc-in form [:fields 0 :options 0 :key] "") languages)
-                         [:fields 0 :options 0 :key])
-                 (get-in (validate-form (assoc-in form [:fields 0 :options 0 :key] nil) languages)
-                         [:fields 0 :options 0 :key])
+          (is (= (get-in (validate-form (assoc-in form [:form/fields 0 :field/options 0 :key] "") languages)
+                         [:form/fields 0 :field/options 0 :key])
+                 (get-in (validate-form (assoc-in form [:form/fields 0 :field/options 0 :key] nil) languages)
+                         [:form/fields 0 :field/options 0 :key])
                  :t.form.validation/required)))
 
         (testing "missing option label"
-          (let [empty-label (validate-form (assoc-in form [:fields 0 :options 0 :label] {:en "" :fi ""}) languages)
-                nil-label (validate-form (assoc-in form [:fields 0 :options 0 :label] nil) languages)]
-            (is (= (get-in empty-label [:fields 0 :options 0 :label :en])
-                   (get-in empty-label [:fields 0 :options 0 :label :fi])
-                   (get-in nil-label [:fields 0 :options 0 :label :en])
-                   (get-in nil-label [:fields 0 :options 0 :label :fi])
+          (let [empty-label (validate-form (assoc-in form [:form/fields 0 :field/options 0 :label] {:en "" :fi ""}) languages)
+                nil-label (validate-form (assoc-in form [:form/fields 0 :field/options 0 :label] nil) languages)]
+            (is (= (get-in empty-label [:form/fields 0 :field/options 0 :label :en])
+                   (get-in empty-label [:form/fields 0 :field/options 0 :label :fi])
+                   (get-in nil-label [:form/fields 0 :field/options 0 :label :en])
+                   (get-in nil-label [:form/fields 0 :field/options 0 :label :fi])
                    :t.form.validation/required))))))))
 
 (deftest build-localized-string-test