diff --git a/src/box/alter.cc b/src/box/alter.cc
index 324b7dc2c533972b2d563320ce981e9695ff1281..9215c07fd55745ee120946aee90bb402c7beb8e2 100644
--- a/src/box/alter.cc
+++ b/src/box/alter.cc
@@ -372,8 +372,9 @@ field_def_decode(struct field_def *field, const char **data,
 		uint32_t key_len;
 		const char *key = mp_decode_str(data, &key_len);
 		if (opts_parse_key(field, field_def_reg, key, key_len, data,
-			       ER_WRONG_SPACE_FORMAT,
-			       fieldno + TUPLE_INDEX_BASE, region) != 0)
+				   ER_WRONG_SPACE_FORMAT,
+				   fieldno + TUPLE_INDEX_BASE, region,
+				   true) != 0)
 			diag_raise();
 	}
 	if (field->name == NULL) {
diff --git a/src/box/opt_def.c b/src/box/opt_def.c
index ebb219c83ea5bd00e922bd2a4124195b0a308bd7..cd93c23b819e2bb1534bef01a5b818802700f888 100644
--- a/src/box/opt_def.c
+++ b/src/box/opt_def.c
@@ -144,7 +144,8 @@ opt_set(void *opts, const struct opt_def *def, const char **val,
 int
 opts_parse_key(void *opts, const struct opt_def *reg, const char *key,
 	       uint32_t key_len, const char **data, uint32_t errcode,
-	       uint32_t field_no, struct region *region)
+	       uint32_t field_no, struct region *region,
+	       bool skip_unknown_options)
 {
 	char errmsg[DIAG_ERRMSG_MAX];
 	bool found = false;
@@ -163,10 +164,14 @@ opts_parse_key(void *opts, const struct opt_def *reg, const char *key,
 		break;
 	}
 	if (!found) {
-		snprintf(errmsg, sizeof(errmsg), "unexpected option '%.*s'",
-			 key_len, key);
-		diag_set(ClientError, errcode, field_no, errmsg);
-		return -1;
+		if (skip_unknown_options) {
+			mp_next(data);
+		} else {
+			snprintf(errmsg, sizeof(errmsg),
+				 "unexpected option '%.*s'", key_len, key);
+			diag_set(ClientError, errcode, field_no, errmsg);
+			return -1;
+		}
 	}
 	return 0;
 }
@@ -195,7 +200,7 @@ opts_decode(void *opts, const struct opt_def *reg, const char **map,
 		uint32_t key_len;
 		const char *key = mp_decode_str(map, &key_len);
 		if (opts_parse_key(opts, reg, key, key_len, map, errcode,
-				   field_no, region) != 0)
+				   field_no, region, false) != 0)
 			return -1;
 	}
 	return 0;
diff --git a/src/box/opt_def.h b/src/box/opt_def.h
index d1109b65c4f6dc3cce4ed7acf6a824e68c26d8d5..e1149b9820a8875918dee69b352f27333c571e1c 100644
--- a/src/box/opt_def.h
+++ b/src/box/opt_def.h
@@ -33,6 +33,7 @@
 
 #include "trivia/util.h"
 #include <stddef.h>
+#include <stdbool.h>
 
 #if defined(__cplusplus)
 extern "C" {
@@ -88,12 +89,24 @@ opts_decode(void *opts, const struct opt_def *reg, const char **map,
 	    uint32_t errcode, uint32_t field_no, struct region *region);
 
 /**
- * Populate one options from msgpack-encoded representation
+ * Decode one option and store it into @a opts struct as a field.
+ * @param opts[out] Options to decode to.
+ * @param reg Options definition.
+ * @param key Name of an option.
+ * @param key_len Length of @a key.
+ * @param data Option value.
+ * @param errcode Code of error to set if something is wrong.
+ * @param field_no Field number of an option in a parent element.
+ * @param region Region to allocate OPT_STRPTR option.
+ * @param skip_unknown_options If true, do not set error, if an
+ *        option is unknown. Useful, when it is neccessary to
+ *        allow to store custom fields in options.
  */
 int
 opts_parse_key(void *opts, const struct opt_def *reg, const char *key,
 	       uint32_t key_len, const char **data, uint32_t errcode,
-	       uint32_t field_no, struct region *region);
+	       uint32_t field_no, struct region *region,
+	       bool skip_unknown_options);
 
 #if defined(__cplusplus)
 } /* extern "C" */
diff --git a/test/box/ddl.result b/test/box/ddl.result
index 7076e836add55f790b4ede5d7dfcb32f4595e56f..3f086f9f67174e53e2d80376048c9cea0f837e66 100644
--- a/test/box/ddl.result
+++ b/test/box/ddl.result
@@ -480,3 +480,28 @@ box.space._collation.index.name:delete{'test'}
 ---
 - [2, 'test', 0, 'ICU', 'ru_RU', {}]
 ...
+--
+-- gh-2839: allow to store custom fields in field definition.
+--
+format = {}
+---
+...
+format[1] = {name = 'field1', type = 'unsigned'}
+---
+...
+format[2] = {'field2', 'unsigned'}
+---
+...
+format[3] = {'field3', 'unsigned', custom_field = 'custom_value'}
+---
+...
+s = box.schema.create_space('test', {format = format})
+---
+...
+s:format()[3].custom_field
+---
+- custom_value
+...
+s:drop()
+---
+...
diff --git a/test/box/ddl.test.lua b/test/box/ddl.test.lua
index f13c54c66682c8a173e84f1f0f7f1274baab3baa..5b1d9dec7ee3fefc0ebdc5e7e85971b347cdef98 100644
--- a/test/box/ddl.test.lua
+++ b/test/box/ddl.test.lua
@@ -186,3 +186,14 @@ box.space._collation:select{}
 test_run:cmd('restart server default')
 box.space._collation:select{}
 box.space._collation.index.name:delete{'test'}
+
+--
+-- gh-2839: allow to store custom fields in field definition.
+--
+format = {}
+format[1] = {name = 'field1', type = 'unsigned'}
+format[2] = {'field2', 'unsigned'}
+format[3] = {'field3', 'unsigned', custom_field = 'custom_value'}
+s = box.schema.create_space('test', {format = format})
+s:format()[3].custom_field
+s:drop()