diff --git a/changelogs/unreleased/gh-8157-default-field-value.md b/changelogs/unreleased/gh-8157-default-field-value.md
new file mode 100644
index 0000000000000000000000000000000000000000..d719476f03f90a5b04a33cec09618f75944f88c3
--- /dev/null
+++ b/changelogs/unreleased/gh-8157-default-field-value.md
@@ -0,0 +1,3 @@
+## feature/core
+
+* Introduced the default field values in the space format (gh-8157).
diff --git a/src/box/errcode.h b/src/box/errcode.h
index bde19049ea5fbbaa17fc033492782ea7622b61e1..9f5b1530ebe57f46a2fea5ea36d0e6c9c9b2b0c4 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -300,7 +300,7 @@ struct errcode_record {
 	/*245 */_(ER_OLD_TERM,			"The term is outdated: old - %llu, new - %llu") \
 	/*246 */_(ER_INTERFERING_ELECTIONS,	"Interfering elections started")\
 	/*247 */_(ER_ITERATOR_POSITION,		"Iterator position is invalid") \
-	/*248 */_(ER_UNUSED,			"") \
+	/*248 */_(ER_DEFAULT_VALUE_TYPE,	"Type of the default value does not match tuple field %s type: expected %s, got %s") \
 	/*249 */_(ER_UNKNOWN_AUTH_METHOD,	"Unknown authentication method '%s'") \
 	/*250 */_(ER_INVALID_AUTH_DATA,		"Invalid '%s' data: %s") \
 	/*251 */_(ER_INVALID_AUTH_REQUEST,	"Invalid '%s' request: %s") \
diff --git a/src/box/field_def.c b/src/box/field_def.c
index 2a82ec9b734ce22d83eb825e34e633598c956a6d..281a4f1a1a6cdce78a1035773a29157bc0f85f00 100644
--- a/src/box/field_def.c
+++ b/src/box/field_def.c
@@ -168,6 +168,14 @@ field_type1_contains_type2(enum field_type type1, enum field_type type2)
 	return field_type_compatibility[idx];
 }
 
+/**
+ * Callback to parse a value with 'default' key in msgpack field definition.
+ * See function definition below.
+ */
+static int
+field_def_parse_default_value(const char **data, void *opts,
+			      struct region *region);
+
 /**
  * Callback to parse a value with 'constraint' key in msgpack field definition.
  * See function definition below.
@@ -195,6 +203,7 @@ static const struct opt_def field_def_reg[] = {
 	OPT_DEF("sql_default", OPT_STRPTR, struct field_def, sql_default_value),
 	OPT_DEF_ENUM("compression", compression_type, struct field_def,
 		     compression_type, NULL),
+	OPT_DEF_CUSTOM("default", field_def_parse_default_value),
 	OPT_DEF_CUSTOM("constraint", field_def_parse_constraint),
 	OPT_DEF_CUSTOM("foreign_key", field_def_parse_foreign_key),
 	OPT_END,
@@ -207,6 +216,8 @@ const struct field_def field_def_default = {
 	.nullable_action = ON_CONFLICT_ACTION_DEFAULT,
 	.coll_id = COLL_NONE,
 	.sql_default_value = NULL,
+	.default_value = NULL,
+	.default_value_size = 0,
 	.constraint_count = 0,
 	.constraint_def = NULL,
 };
@@ -228,6 +239,30 @@ field_type_by_name(const char *name, size_t len)
 	return field_type_MAX;
 }
 
+/**
+ * Parse default field value from msgpack.
+ * Used as callback to parse a value with 'default' key in field definition.
+ * Move @a data msgpack pointer to the end of msgpack value.
+ * By convention @a opts must point to corresponding struct field_def.
+ * Allocate a temporary copy of a default value on @a region and set pointer to
+ * it as field_def->default_value, also setting field_def->default_value_size.
+ */
+static int
+field_def_parse_default_value(const char **data, void *opts,
+			      struct region *region)
+{
+	struct field_def *def = (struct field_def *)opts;
+	const char *default_value = *data;
+	mp_next(data);
+	const char *default_value_end = *data;
+	size_t size = default_value_end - default_value;
+
+	def->default_value = xregion_alloc(region, size);
+	def->default_value_size = size;
+	memcpy(def->default_value, default_value, size);
+	return 0;
+}
+
 /**
  * Parse constraint array from msgpack.
  * Used as callback to parse a value with 'constraint' key in field definition.
@@ -399,6 +434,7 @@ field_def_array_dup(const struct field_def *fields, uint32_t field_count)
 			grp_alloc_reserve_str0(&all,
 					       fields[i].sql_default_value);
 		}
+		grp_alloc_reserve_data(&all, fields[i].default_value_size);
 	}
 	grp_alloc_use(&all, xmalloc(grp_alloc_size(&all)));
 	struct field_def *copy = grp_alloc_create_data(
@@ -410,6 +446,12 @@ field_def_array_dup(const struct field_def *fields, uint32_t field_count)
 			copy[i].sql_default_value = grp_alloc_create_str0(
 				&all, fields[i].sql_default_value);
 		}
+		if (fields[i].default_value != NULL) {
+			size_t size = fields[i].default_value_size;
+			char *buf = grp_alloc_create_data(&all, size);
+			memcpy(buf, fields[i].default_value, size);
+			copy[i].default_value = buf;
+		}
 		copy[i].constraint_def = tuple_constraint_def_array_dup(
 			fields[i].constraint_def, fields[i].constraint_count);
 	}
diff --git a/src/box/field_def.h b/src/box/field_def.h
index 6b0fe6677390a8bf7757f81e17b9fe510847dc04..1a9438fe5064cfbe5acc98bd7c825413126e92ab 100644
--- a/src/box/field_def.h
+++ b/src/box/field_def.h
@@ -148,6 +148,10 @@ struct field_def {
 	uint32_t coll_id;
 	/** 0-terminated SQL expression for DEFAULT value. */
 	char *sql_default_value;
+	/** MsgPack with the default value. */
+	char *default_value;
+	/** Size of the default value. */
+	size_t default_value_size;
 	/** Compression type for this field. */
 	enum compression_type compression_type;
 	/** Array of constraints. Can be NULL if constraints_count == 0. */
diff --git a/src/box/request.c b/src/box/request.c
index 4c677c7304c43ce19fdfe35ee393f585ac1c10ac..4c923c6ac32a4beb04f1ca848d551e054ebd9fea 100644
--- a/src/box/request.c
+++ b/src/box/request.c
@@ -69,13 +69,17 @@ request_update_header(struct request *request, struct xrow_header *row,
 
 int
 request_create_from_tuple(struct request *request, struct space *space,
-			  struct tuple *old_tuple, struct tuple *new_tuple,
-			  struct region *region)
+			  const char *old_data, uint32_t old_size,
+			  const char *new_data, uint32_t new_size,
+			  struct region *region, bool preserve_request_type)
 {
+	const char *upsert_ops = request->ops;
+	const char *upsert_ops_end = request->ops_end;
+	enum iproto_type request_type = request->type;
 	struct xrow_header *row = request->header;
 	memset(request, 0, sizeof(*request));
 
-	if (old_tuple == new_tuple) {
+	if (old_data == new_data) {
 		/*
 		 * Old and new tuples are the same,
 		 * turn this request into no-op.
@@ -89,11 +93,10 @@ request_create_from_tuple(struct request *request, struct space *space,
 	 * this line is not reached.
 	 */
 	request->space_id = space->def->id;
-	if (new_tuple == NULL) {
-		uint32_t size, key_size;
-		const char *data = tuple_data_range(old_tuple, &size);
+	if (new_data == NULL) {
+		uint32_t key_size;
 		request->key = tuple_extract_key_raw_to_region(
-					data, data + size,
+					old_data, old_data + old_size,
 					space->index[0]->def->key_def,
 					MULTIKEY_NONE, &key_size, region);
 		if (request->key == NULL)
@@ -101,8 +104,6 @@ request_create_from_tuple(struct request *request, struct space *space,
 		request->key_end = request->key + key_size;
 		request->type = IPROTO_DELETE;
 	} else {
-		uint32_t size;
-		const char *data = tuple_data_range(new_tuple, &size);
 		/*
 		 * We have to copy the tuple data to region, because
 		 * the tuple is allocated on runtime arena and not
@@ -110,15 +111,16 @@ request_create_from_tuple(struct request *request, struct space *space,
 		 * current transaction ends while we need to write
 		 * the tuple data to WAL on commit.
 		 */
-		char *buf = region_alloc(region, size);
-		if (buf == NULL) {
-			diag_set(OutOfMemory, size, "region_alloc", "tuple");
-			return -1;
-		}
-		memcpy(buf, data, size);
+		char *buf = xregion_alloc(region, new_size);
+		memcpy(buf, new_data, new_size);
 		request->tuple = buf;
-		request->tuple_end = buf + size;
-		request->type = IPROTO_REPLACE;
+		request->tuple_end = buf + new_size;
+		request->type = preserve_request_type ? request_type :
+							IPROTO_REPLACE;
+		if (request->type == IPROTO_UPSERT) {
+			request->ops = upsert_ops;
+			request->ops_end = upsert_ops_end;
+		}
 	}
 	request_update_header(request, row, region);
 	return 0;
diff --git a/src/box/request.h b/src/box/request.h
index a1ffd15c63486dd072e6fb1484b69485587a5b04..acf0226c2aac05ae377e34519c0c7b55e4ac0d1f 100644
--- a/src/box/request.h
+++ b/src/box/request.h
@@ -30,6 +30,8 @@
  * THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
  * SUCH DAMAGE.
  */
+#include <stdbool.h>
+#include <stdint.h>
 
 #if defined(__cplusplus)
 extern "C" {
@@ -46,18 +48,24 @@ struct region;
  *
  * @param request - request to fix
  * @param space - space corresponding to request
- * @param old_tuple - the old tuple
- * @param new_tuple - the new tuple
+ * @param old_data - the old tuple data
+ * @param old_size - size of the old data
+ * @param new_data - the new tuple data
+ * @param new_size - size of the new data
  * @param region - region where request parts will be allocated
+ * @param preserve_insert_type - do not turn INSERT requests into REPLACE
  *
- * If old_tuple and new_tuple are the same, the request is turned into NOP.
- * If new_tuple is NULL, the request is turned into DELETE(old_tuple).
- * If new_tuple is not NULL, the request is turned into REPLACE(new_tuple).
+ * If old_data and new_data are the same, the request is turned into NOP.
+ * If new_data is NULL, the request is turned into DELETE(old_data).
+ * If new_data is not NULL, the request is turned into REPLACE(new_data),
+ * or into INSERT(new_data)/UPSERT(new_data) when preserve_request_type is true
+ * and the original request was INSERT or UPSERT.
  */
 int
 request_create_from_tuple(struct request *request, struct space *space,
-			  struct tuple *old_tuple, struct tuple *new_tuple,
-			  struct region *region);
+			  const char *old_data, uint32_t old_size,
+			  const char *new_data, uint32_t new_size,
+			  struct region *region, bool preserve_request_type);
 
 /**
  * Convert a request accessing a secondary key to a primary
diff --git a/src/box/space.c b/src/box/space.c
index 5477a58d907fbf7d4fad3d788b4201a621ac20e1..58dfd41726cdf1c56cab2fa82b2fe2bbe56705d2 100644
--- a/src/box/space.c
+++ b/src/box/space.c
@@ -516,6 +516,41 @@ space_index_def(struct space *space, int n)
 	return space->index[n]->def;
 }
 
+/**
+ * Apply default values from the space format to the null (or absent) fields of
+ * request->tuple.
+ */
+static int
+space_apply_defaults(struct space *space, struct txn *txn,
+		     struct request *request)
+{
+	assert(request->type == IPROTO_INSERT ||
+	       request->type == IPROTO_REPLACE ||
+	       request->type == IPROTO_UPSERT);
+
+	const char *new_data = request->tuple;
+	const char *new_data_end = request->tuple_end;
+	size_t region_svp = region_used(&fiber()->gc);
+
+	bool changed = tuple_format_apply_defaults(space->format, &new_data,
+						   &new_data_end);
+	if (!changed) {
+		region_truncate(&fiber()->gc, region_svp);
+		return 0;
+	}
+	/*
+	 * Field defaults changed the resulting tuple.
+	 * Fix the request to conform.
+	 */
+	struct region *txn_region = tx_region_acquire(txn);
+	int rc = request_create_from_tuple(request, space, NULL, 0, new_data,
+					   new_data_end - new_data, txn_region,
+					   true);
+	tx_region_release(txn, TX_ALLOC_SYSTEM);
+	region_truncate(&fiber()->gc, region_svp);
+	return rc;
+}
+
 /**
  * Run BEFORE triggers and foreign key constraint checks registered for a space.
  * If a trigger changes the current statement, this function updates the
@@ -584,13 +619,18 @@ after_old_tuple_lookup:;
 	/*
 	 * Create the new tuple.
 	 */
-	uint32_t new_size, old_size;
+	uint32_t new_size, old_size = 0;
 	const char *new_data, *new_data_end;
-	const char *old_data, *old_data_end;
+	const char *old_data = NULL, *old_data_end;
 
 	switch (request->type) {
 	case IPROTO_INSERT:
+		new_data = request->tuple;
+		new_data_end = request->tuple_end;
+		break;
 	case IPROTO_REPLACE:
+		if (old_tuple != NULL)
+			old_data = tuple_data_range(old_tuple, &old_size);
 		new_data = request->tuple;
 		new_data_end = request->tuple_end;
 		break;
@@ -615,6 +655,7 @@ after_old_tuple_lookup:;
 			/* Nothing to delete. */
 			return 0;
 		}
+		old_data = tuple_data_range(old_tuple, &old_size);
 		new_data = new_data_end = NULL;
 		break;
 	case IPROTO_UPSERT:
@@ -700,7 +741,7 @@ after_old_tuple_lookup:;
 	int rc = trigger_run(&space->before_replace, txn);
 
 	/*
-	 * BEFORE riggers cannot change the old tuple,
+	 * BEFORE triggers cannot change the old tuple,
 	 * but they may replace the new tuple.
 	 */
 	bool request_changed = (stmt->new_tuple != new_tuple);
@@ -731,10 +772,13 @@ after_old_tuple_lookup:;
 	 * Fix the request to conform.
 	 */
 	if (request_changed) {
+		new_data = new_tuple == NULL ? NULL :
+			   tuple_data_range(new_tuple, &new_size);
 		struct region *txn_region = tx_region_acquire(txn);
 		rc = request_create_from_tuple(request, space,
-					       old_tuple, new_tuple,
-					       txn_region);
+					       old_data, old_size,
+					       new_data, new_size,
+					       txn_region, false);
 		tx_region_release(txn, TX_ALLOC_SYSTEM);
 	}
 out:
@@ -774,6 +818,16 @@ space_execute_dml(struct space *space, struct txn *txn,
 			}
 		}
 	}
+
+	bool need_defaults_apply = tuple_format_has_defaults(space->format) &&
+				   recovery_state == FINISHED_RECOVERY &&
+				   request->type != IPROTO_UPDATE &&
+				   request->type != IPROTO_DELETE;
+	if (unlikely(need_defaults_apply)) {
+		if (space_apply_defaults(space, txn, request) != 0)
+			return -1;
+	}
+
 	if (unlikely((!rlist_empty(&space->before_replace) &&
 		      space->run_triggers) || need_foreign_key_check)) {
 		/*
diff --git a/src/box/sql.c b/src/box/sql.c
index a5248a30fc471fe2a20e8f18aa10630c5deb0498..0da9c1e572f40228fb9a84be704ded2361d4c880 100644
--- a/src/box/sql.c
+++ b/src/box/sql.c
@@ -378,6 +378,8 @@ sql_ephemeral_space_new(const struct sql_space_info *info)
 		fields[i].is_nullable = true;
 		fields[i].nullable_action = ON_CONFLICT_ACTION_NONE;
 		fields[i].sql_default_value = NULL;
+		fields[i].default_value = NULL;
+		fields[i].default_value_size = 0;
 		fields[i].type = info->types[i];
 		fields[i].coll_id = info->coll_ids[i];
 		fields[i].compression_type = COMPRESSION_TYPE_NONE;
diff --git a/src/box/tuple_format.c b/src/box/tuple_format.c
index acf6938d14244f2608837599ffebec3d55786bd4..353da7972c826b4784277e025d89e092957d2d4c 100644
--- a/src/box/tuple_format.c
+++ b/src/box/tuple_format.c
@@ -34,6 +34,7 @@
 #include "json/json.h"
 #include "coll_id_cache.h"
 #include "trivia/util.h"
+#include "tuple_builder.h"
 #include "tuple_constraint.h"
 #include "tt_static.h"
 
@@ -181,6 +182,8 @@ tuple_field_new(void)
 	field->multikey_required_fields = NULL;
 	field->constraint_count = 0;
 	field->constraint = NULL;
+	field->default_value = NULL;
+	field->default_value_size = 0;
 	return field;
 }
 
@@ -193,6 +196,7 @@ tuple_field_delete(struct tuple_field *field)
 	free(field->constraint);
 	if (field->sql_default_value_expr != NULL)
 		tuple_format_expr_delete(field->sql_default_value_expr);
+	free(field->default_value);
 	free(field);
 }
 
@@ -510,6 +514,25 @@ tuple_format_create(struct tuple_format *format, struct key_def *const *keys,
 			if (field->sql_default_value_expr == NULL)
 				return -1;
 		}
+		char *default_value = fields[i].default_value;
+		if (default_value != NULL) {
+			bool is_compatible = field_mp_type_is_compatible(
+				field->type, default_value, false);
+			if (!is_compatible) {
+				enum mp_type type = mp_typeof(*default_value);
+				diag_set(ClientError, ER_DEFAULT_VALUE_TYPE,
+					 tuple_field_path(field, format),
+					 field_type_strs[field->type],
+					 mp_type_strs[type]);
+				return -1;
+			}
+			size_t size = fields[i].default_value_size;
+			char *buf = xmalloc(size);
+			memcpy(buf, default_value, size);
+			field->default_value = buf;
+			field->default_value_size = size;
+			format->default_field_count = i + 1;
+		}
 	}
 
 	int current_slot = 0;
@@ -719,6 +742,7 @@ tuple_format_alloc(struct key_def * const *keys, uint16_t key_count,
 	format->epoch = 0;
 	format->constraint_count = 0;
 	format->constraint = NULL;
+	format->default_field_count = 0;
 	return format;
 error:
 	tuple_format_destroy_fields(format);
@@ -1018,9 +1042,10 @@ tuple_field_map_create_plain(struct tuple_format *format, const char *tuple,
 		mp_next(&next_pos);
 		field = json_tree_entry(*token, struct tuple_field, token);
 		if (validate) {
-			bool nullable = tuple_field_is_nullable(field);
+			bool allow_null = tuple_field_is_nullable(field) ||
+					  tuple_field_has_default(field);
 			if(!field_mp_type_is_compatible(field->type, pos,
-							nullable)) {
+							allow_null)) {
 				diag_set(ClientError, ER_FIELD_TYPE,
 					 tuple_field_path(field, format),
 					 field_type_strs[field->type],
@@ -1397,3 +1422,69 @@ tuple_format_iterator_next(struct tuple_format_iterator *it,
 	entry->data = NULL;
 	return 0;
 }
+
+bool
+tuple_format_apply_defaults(struct tuple_format *format, const char **data,
+			    const char **data_end)
+{
+	struct tuple_builder builder;
+	tuple_builder_new(&builder, &fiber()->gc);
+	bool is_tuple_changed = false;
+	/*
+	 * Process fields that are present in both the format and the tuple.
+	 * Break prematurely when all defaults are applied.
+	 */
+	const char *p = *data;
+	uint32_t tuple_field_count = mp_decode_array(&p);
+	size_t i;
+	for (i = 0; i < MIN(tuple_field_count,
+			    format->default_field_count); i++) {
+		const char *p_next = p;
+		mp_next(&p_next);
+
+		struct tuple_field *field = NULL;
+		bool is_null = mp_typeof(*p) == MP_NIL;
+		if (is_null)
+			field = tuple_format_field(format, i);
+
+		if (is_null && tuple_field_has_default(field)) {
+			tuple_builder_add(&builder, field->default_value,
+					  field->default_value_size, 1);
+			is_tuple_changed = true;
+		} else {
+			tuple_builder_add(&builder, p, p_next - p, 1);
+		}
+		p = p_next;
+	}
+	/*
+	 * Process fields that are present in the format, but not in the tuple.
+	 * Break prematurely when all defaults are applied.
+	 */
+	for ( ; i < format->default_field_count; i++) {
+		struct tuple_field *field = tuple_format_field(format, i);
+		if (tuple_field_has_default(field)) {
+			tuple_builder_add(&builder, field->default_value,
+					  field->default_value_size, 1);
+			is_tuple_changed = true;
+		} else {
+			tuple_builder_add_nil(&builder);
+		}
+	}
+	/*
+	 * Return if no fields were changed.
+	 */
+	if (!is_tuple_changed)
+		return false;
+	/*
+	 * If the tuple has more fields, append them as is.
+	 */
+	if (tuple_field_count > i) {
+		uint32_t field_count = tuple_field_count - i;
+		tuple_builder_add(&builder, p, *data_end - p, field_count);
+	}
+	/*
+	 * Allocate a buffer and encode all elements into the new MsgPack array.
+	 */
+	tuple_builder_finalize(&builder, data, data_end);
+	return true;
+}
diff --git a/src/box/tuple_format.h b/src/box/tuple_format.h
index eb4a1eb0672bfe298566c47060d4d283d76dbc6e..8f320ae737f8031e7a2c97832ff86ad1da46bb4c 100644
--- a/src/box/tuple_format.h
+++ b/src/box/tuple_format.h
@@ -65,7 +65,6 @@ enum { TUPLE_INDEX_BASE = 1 };
 enum { TUPLE_OFFSET_SLOT_NIL = INT32_MAX };
 
 struct tuple;
-struct tuple_chunk;
 struct tuple_format;
 struct coll;
 struct Expr;
@@ -163,6 +162,10 @@ struct tuple_field {
 	uint32_t constraint_count;
 	/** AST for parsed SQL default value. */
 	struct Expr *sql_default_value_expr;
+	/** MsgPack with the default value. */
+	char *default_value;
+	/** Size of the default value. */
+	size_t default_value_size;
 };
 
 /**
@@ -177,6 +180,15 @@ tuple_field_is_nullable(const struct tuple_field *tuple_field)
 	return tuple_field->nullable_action == ON_CONFLICT_ACTION_NONE;
 }
 
+/**
+ * Return true if tuple_field has a default value.
+ */
+static inline bool
+tuple_field_has_default(const struct tuple_field *tuple_field)
+{
+	return tuple_field->default_value != NULL;
+}
+
 /**
  * Return path to a tuple field. Used for error reporting.
  */
@@ -252,6 +264,11 @@ struct tuple_format {
 	 * path fields. See also tuple_format::fields.
 	 */
 	uint32_t total_field_count;
+	/**
+	 * An upper bound for the number of fields with a default value.
+	 * In other words, max fieldno with a default value + 1.
+	 */
+	uint32_t default_field_count;
 	/**
 	 * Bitmap of fields that must be present in a tuple
 	 * conforming to the format. Indexed by tuple_field::id.
@@ -438,6 +455,15 @@ tuple_format_min_field_count(struct key_def * const *keys, uint16_t key_count,
 			     const struct field_def *space_fields,
 			     uint32_t space_field_count);
 
+/**
+ * Return true if format has at least one field with a default value.
+ */
+static inline bool
+tuple_format_has_defaults(const struct tuple_format *format)
+{
+	return format->default_field_count > 0;
+}
+
 typedef struct tuple_format box_tuple_format_t;
 
 /** \cond public */
@@ -641,6 +667,17 @@ int
 tuple_format_iterator_next(struct tuple_format_iterator *it,
 			   struct tuple_format_iterator_entry *entry);
 
+/**
+ * Replace null (or absent) fields of msgpack with the default values from the
+ * format. The input msgpack is located at [*data .. *data_end).
+ * Return true if at least one field is changed, in that case data and data_end
+ * are updated to point to the new buffer with the modified msgpack. The buffer
+ * is allocated on current fiber's region.
+ */
+bool
+tuple_format_apply_defaults(struct tuple_format *format, const char **data,
+			    const char **data_end);
+
 #if defined(__cplusplus)
 } /* extern "C" */
 #endif /* defined(__cplusplus) */
diff --git a/test/box/error.result b/test/box/error.result
index c0973dd006de22ad181b8b58e7748e15eab0e322..b0e1cab38464247f2f9536f559d7b3f45b5982b7 100644
--- a/test/box/error.result
+++ b/test/box/error.result
@@ -466,6 +466,7 @@ t;
  |   245: box.error.OLD_TERM
  |   246: box.error.INTERFERING_ELECTIONS
  |   247: box.error.ITERATOR_POSITION
+ |   248: box.error.DEFAULT_VALUE_TYPE
  |   249: box.error.UNKNOWN_AUTH_METHOD
  |   250: box.error.INVALID_AUTH_DATA
  |   251: box.error.INVALID_AUTH_REQUEST
diff --git a/test/engine-luatest/gh_8157_default_field_value_test.lua b/test/engine-luatest/gh_8157_default_field_value_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..3131a55dd41c77339c1a224f0ab262857ff5f677
--- /dev/null
+++ b/test/engine-luatest/gh_8157_default_field_value_test.lua
@@ -0,0 +1,283 @@
+local t = require('luatest')
+local server = require('luatest.server')
+
+local function before_all(cg)
+    cg.server = server:new({alias = 'master'})
+    cg.server:start()
+end
+
+local function after_all(cg)
+    cg.server:drop()
+end
+
+local function after_each(cg)
+    cg.server:exec(function()
+        if box.space.test then box.space.test:drop() end
+    end)
+end
+
+local g = t.group('gh-8157-1', {{engine = 'memtx'}, {engine = 'vinyl'}})
+g.before_all(before_all)
+g.after_all(after_all)
+g.after_each(after_each)
+
+-- Test default field values.
+g.test_basics = function(cg)
+    cg.server:exec(function(engine)
+        local format = {
+            {name='c1', type='any'},
+            {name='c2', type='any', default={key='val'}},
+            {name='id', type='unsigned', default=0},
+            {name='c4', type='integer', is_nullable=true, default=-100500},
+            {name='c5', type='unsigned', is_nullable=false, default=0},
+            {name='c6', type='string', is_nullable=true, default='hello'}
+        }
+        local opts = {engine = engine, format = format}
+        local s = box.schema.space.create('test', opts)
+        s:create_index('pk', {parts={'id'}})
+        local sk1 = s:create_index('sk1', {parts={'c4'}})
+        local sk2 = s:create_index('sk2', {parts={{'c5'}, {'c6'}},
+                                           unique=false})
+        -- c5 and c6 are null
+        s:insert{11, 12, 13, 14}
+        t.assert_equals(sk1:select{14}, {{11, 12, 13, 14, 0, 'hello'}})
+
+        -- c4 is null
+        s:insert{21, 22, 23, nil, 25, '26'}
+        t.assert_equals(sk2:select{25}, {{21, 22, 23, -100500, 25, '26'}})
+
+        -- c2, c5, c6 are null
+        s:insert{nil, nil, 33, 34, box.NULL}
+        t.assert_equals(s:select{33}, {{nil, {key='val'}, 33, 34, 0, 'hello'}})
+
+        -- c6 is null + more fields
+        s:insert{41, 42, 43, 44, 45, nil, ',', 'world', '!'}
+        t.assert_equals(s:select{43}, {{41, 42, 43, 44, 45, 'hello',
+                                        ',', 'world', '!'}})
+
+        -- Primary indexed field id is null
+        s:insert{51, 52, nil, 54, 55, ''}
+        t.assert_equals(s:select{0}, {{51, 52, 0, 54, 55, ''}})
+    end, {cg.params.engine})
+end
+
+-- Test default field values with UPDATE and UPSERT operations.
+g.test_update_upsert = function(cg)
+    cg.server:exec(function(engine)
+        local s = box.schema.space.create('test', {engine=engine})
+        s:create_index('pk')
+        s:insert{1000, nil, 'qwerty'}
+
+        -- Check that s:format(format) is successful although the space contains
+        -- a tuple with non-nullable field `name', which is null.
+        local format = {{name='id', type='unsigned'},
+                        {name='name', type='string', default='Noname'},
+                        {name='pass', type='string'},
+                        {name='shell', type='string', default='/bin/sh',
+                         is_nullable=true}}
+        t.assert_equals(s:format(format), nil)
+
+        -- Note that existing tuples are not updated automatically by s:format()
+        t.assert_equals(s:select{1000}, {{1000, nil, 'qwerty'}})
+
+        -- Check that UPDATE does not change the `name' field, which is null.
+        s:update({1000}, {{'=', 'pass', 'secret'}})
+        t.assert_equals(s:select{1000}, {{1000, nil, 'secret'}})
+
+        -- Test UPSERT (acts as UPDATE)
+        s:upsert({1000, nil, 'love'}, {{'=', 'pass', '123456'}})
+        t.assert_equals(s:select{1000}, {{1000, nil, '123456'}})
+
+        -- Test UPSERT (acts as INSERT)
+        s:upsert({1001, nil, 'god'}, {})
+        t.assert_equals(s:select{1001}, {{1001, 'Noname', 'god', '/bin/sh'}})
+    end, {cg.params.engine})
+end
+
+-- Test default field values in a space with the field_count option set.
+g.test_exact_field_count = function(cg)
+    cg.server:exec(function(engine)
+        local format = {{name='id', type='integer'},
+                        {name='id1', type='integer'},
+                        {name='id2', type='integer', default=0}}
+        local opts = {engine = engine, format = format, field_count = 3}
+        local s = box.schema.space.create('test', opts)
+        s:create_index('pk')
+
+        -- Default value is applied to field 3 (id2), but field 2 is null.
+        t.assert_error_msg_content_equals('Tuple field 2 (id1) type does ' ..
+            'not match one required by operation: expected integer, got nil',
+            s.insert, s, {1})
+
+        t.assert_equals(s:insert{2, 2}, {2, 2, 0})
+        t.assert_equals(s:insert{3, 3, 3}, {3, 3, 3})
+
+        t.assert_error_msg_content_equals(
+            'Tuple field count 4 does not match space field count 3',
+            s.insert, s, {4, 4, nil, 4})
+    end, {cg.params.engine})
+end
+
+-- Test error messages.
+g.test_errors = function(cg)
+    cg.server:exec(function(engine)
+        -- Bad type of the default value.
+        local format = {{name='id', type='integer'},
+                        {name='c2', type='integer', default='not_integer'}}
+        local opts = {engine = engine, format = format}
+        t.assert_error_msg_content_equals('Type of the default value does ' ..
+            'not match tuple field 2 (c2) type: expected integer, got string',
+            box.schema.space.create, 'test', opts)
+
+        format = {{name='id', type='integer'},
+                  {name='c2', type='integer', default=-1}}
+        opts = {engine = engine, format = format}
+        local s = box.schema.space.create('test', opts)
+
+        -- Check upsert into a space without the primary index.
+        t.assert_error_msg_content_equals(
+            "No index #0 is defined in space 'test'",
+            s.upsert, s, {1}, {})
+
+        -- Check upsert with a wrong key type.
+        s:create_index('pk')
+        s:create_index('sk', {parts={'c2'}})
+        t.assert_error_msg_content_equals('Tuple field 1 (id) type does not ' ..
+            'match one required by operation: expected integer, got string',
+            s.upsert, s, {'bad'}, {})
+
+        -- Check upsert with a wrong update operation.
+        t.assert_error_msg_content_equals(
+            'Illegal parameters, update operation must be an array {op,..}',
+            s.upsert, s, {0}, {'bad'})
+        s:insert{0}
+        t.assert_error_msg_content_equals(
+            'Illegal parameters, update operation must be an array {op,..}',
+            s.upsert, s, {0}, {'bad'})
+
+        -- Check "duplicate key exists" error messages.
+        t.assert_error_msg_content_equals('Duplicate key exists in unique ' ..
+            'index "pk" in space "test" with old tuple - [0, -1] and new ' ..
+            'tuple - [0, -1]', s.insert, s, {0})
+        t.assert_error_msg_content_equals('Duplicate key exists in unique ' ..
+            'index "sk" in space "test" with old tuple - [0, -1] and new ' ..
+            'tuple - [1, -1]', s.insert, s, {1})
+        s:truncate()
+
+        -- Space format has more fields than the inserted tuple.
+        -- Check that the error message complains only about c3.
+        s:format{{name='id', type='integer', default=0},
+                 {name='c2', type='integer', default=-1},
+                 {name='c3', type='integer'}}
+        t.assert_error_msg_content_equals(
+            'Tuple field 3 (c3) required by space format is missing',
+            s.insert, s, {0})
+    end, {cg.params.engine})
+end
+
+-- Test default field values in conjunction with before_replace triggers.
+g.test_triggers = function(cg)
+    cg.server:exec(function(engine)
+        local format = {{name='id', type='unsigned'},
+                        {name='name', type='string', default='Noname'},
+                        {name='pass', type='string'},
+                        {name='shell', type='string', default='/bin/sh'}}
+        local s = box.schema.space.create('test', {engine = engine})
+        s:create_index('pk')
+        s:insert{1000, nil, '0000', '/bin/nologin'}
+        s:format(format)
+
+        local trigger1 = function(_, new)
+            return box.tuple.update(new, {{'=', 3, 'hacked'}})
+        end
+        s:before_replace(trigger1)
+
+        -- Check that UPSERT (acts as INSERT) applies default values.
+        t.assert_equals(s:upsert({1001, nil, 'xxxxxxxxx'}, {}),
+                        {1001, 'Noname', 'hacked', '/bin/sh'})
+
+        -- Check that UPSERT (acts as UPDATE) doesn't apply the default.
+        t.assert_equals(s:upsert({1000, nil, '0000', '/bin/zsh'},
+                                 {{'=', 'shell', '/bin/zsh'}}),
+                        {1000, nil, 'hacked', '/bin/zsh'})
+
+        -- Check that REPLACE applies default values.
+        t.assert_equals(s:replace{1000, nil, '123'},
+                        {1000, 'Noname', 'hacked', '/bin/sh'})
+
+        -- Check that defaults can be applied inside a trigger.
+        local trigger2 = function(_, new)
+            box.space.test:run_triggers(false)
+            box.space.test:insert{9000, nil, new[3]}
+        end
+        s:before_replace(trigger2, trigger1)
+        s:insert{1002, 'user', 'secret'}
+        t.assert_equals(s:select{9000}, {{9000, 'Noname', 'secret', '/bin/sh'}})
+    end, {cg.params.engine})
+end
+
+-- Test default field values with access via net.box.
+g.test_netbox = function(cg)
+    cg.server:exec(function(engine)
+        local format = {{name='id1', type='integer', default=1000},
+                        {name='id2', type='integer', default=2000},
+                        {name='id3', type='integer', default=3000}}
+        local opts = {engine = engine, format = format}
+        local s = box.schema.space.create('test', opts)
+        s:create_index('pk')
+    end, {cg.params.engine})
+
+    local netbox = require('net.box')
+    local c = netbox.connect(cg.server.net_box_uri)
+    local s = c.space.test
+
+    t.assert_equals(s:insert{1, nil, 1}, {1, 2000, 1})
+    t.assert_equals(s:insert{2}, {2, 2000, 3000})
+    t.assert_equals(s:insert{}, {1000, 2000, 3000})
+end
+
+local g2 = t.group('gh-8157-2')
+g2.before_all(before_all)
+g2.after_all(after_all)
+g2.after_each(after_each)
+
+-- Test default field values after recovery from xlog and snap.
+g2.test_recovery = function(cg)
+    cg.server:exec(function()
+        local format = {{name='id', type='integer'},
+                        {name='name', type='string', default='guest'}}
+        local opts = {format = format, id = 666}
+        local s = box.schema.space.create('test', opts)
+        s:create_index('pk')
+        s:insert{-1}
+        s:upsert({-2}, {})
+    end)
+
+    -- Check recovery from xlog.
+    cg.server:restart()
+    cg.server:exec(function()
+        t.assert_equals(box.space.test:select{-1}, {{-1, 'guest'}})
+        t.assert_equals(box.space.test:select{-2}, {{-2, 'guest'}})
+    end)
+
+    -- Check that xlog contains the actual default values.
+    local fio = require('fio')
+    local xlog = require('xlog')
+    local xlog_tuples = {}
+    local xlog_path = fio.pathjoin(cg.server.workdir,
+                                   string.format("%020d.xlog", 0))
+    for _, row in xlog.pairs(xlog_path) do
+        if row.BODY.space_id == 666 then
+            table.insert(xlog_tuples, row.BODY.tuple)
+        end
+    end
+    t.assert_equals(xlog_tuples, {{-1, 'guest'}, {-2, 'guest'}})
+
+    --- Check recovery from snap.
+    cg.server:eval('box.snapshot()')
+    cg.server:restart()
+    cg.server:exec(function()
+        t.assert_equals(box.space.test:select{-1}, {{-1, 'guest'}})
+        t.assert_equals(box.space.test:select{-2}, {{-2, 'guest'}})
+    end)
+end