diff --git a/src/box/errcode.h b/src/box/errcode.h
index af28a92651f0b2971f8d74bfffd183c099df245b..3d00c2941f8c65fa5331e5925e3db871dbc25674 100644
--- a/src/box/errcode.h
+++ b/src/box/errcode.h
@@ -91,7 +91,7 @@ struct errcode_record {
 	/* 36 */_(ER_NO_SUCH_SPACE,		"Space '%s' does not exist") \
 	/* 37 */_(ER_NO_SUCH_FIELD,		"Field %d was not found in the tuple") \
 	/* 38 */_(ER_EXACT_FIELD_COUNT,		"Tuple field count %u does not match space field count %u") \
-	/* 39 */_(ER_INDEX_FIELD_COUNT,		"Tuple field count %u is less than required by a defined index (expected %u)") \
+	/* 39 */_(ER_INDEX_FIELD_COUNT,		"Tuple field count %u is less than required (expected at least %u)") \
 	/* 40 */_(ER_WAL_IO,			"Failed to write to disk") \
 	/* 41 */_(ER_MORE_THAN_ONE_TUPLE,	"Get() doesn't support partial keys and non-unique indexes") \
 	/* 42 */_(ER_ACCESS_DENIED,		"%s access on %s is denied for user '%s'") \
diff --git a/src/box/tuple.c b/src/box/tuple.c
index 6bfea3e6a5ef208b514b9b289174969eba18c2de..e6dd5db44ede9d07411146334e992035a50e1803 100644
--- a/src/box/tuple.c
+++ b/src/box/tuple.c
@@ -130,16 +130,18 @@ tuple_validate_raw(struct tuple_format *format, const char *tuple)
 			 (unsigned) format->exact_field_count);
 		return -1;
 	}
-	if (unlikely(field_count < format->field_count)) {
+	if (unlikely(field_count < format->min_field_count)) {
 		diag_set(ClientError, ER_INDEX_FIELD_COUNT,
 			 (unsigned) field_count,
-			 (unsigned) format->field_count);
+			 (unsigned) format->min_field_count);
 		return -1;
 	}
 
 	/* Check field types */
-	for (uint32_t i = 0; i < format->field_count; i++) {
-		struct tuple_field *field = &format->fields[i];
+	struct tuple_field *field = &format->fields[0];
+	uint32_t i = 0;
+	uint32_t defined_field_count = MIN(field_count, format->field_count);
+	for (; i < defined_field_count; ++i, ++field) {
 		if (key_mp_type_validate(field->type, mp_typeof(*tuple),
 					 ER_FIELD_TYPE, i + TUPLE_INDEX_BASE,
 					 field->is_nullable))
diff --git a/src/box/tuple_format.c b/src/box/tuple_format.c
index 9e08df5e51b2006d1f1d86488ed85a3a2087f8e8..819cdc71edda6c66fd980a09147b489ea2983d84 100644
--- a/src/box/tuple_format.c
+++ b/src/box/tuple_format.c
@@ -137,6 +137,8 @@ tuple_format_create(struct tuple_format *format, struct key_def * const *keys,
 		if (tuple_format_add_name(format, name_pos, len, i, true) != 0)
 			return -1;
 		name_pos += len + 1;
+		if (i + 1 > format->min_field_count && !fields[i].is_nullable)
+			format->min_field_count = i + 1;
 	}
 	/* Initialize remaining fields */
 	for (uint32_t i = field_count; i < format->field_count; i++)
@@ -303,6 +305,7 @@ tuple_format_alloc(struct key_def * const *keys, uint16_t key_count,
 	format->field_count = field_count;
 	format->index_field_count = index_field_count;
 	format->exact_field_count = 0;
+	format->min_field_count = index_field_count;
 	return format;
 
 error_name_hash_reserve:
@@ -453,10 +456,10 @@ tuple_init_field_map(const struct tuple_format *format, uint32_t *field_map,
 			 (unsigned) format->exact_field_count);
 		return -1;
 	}
-	if (unlikely(field_count < format->field_count)) {
+	if (unlikely(field_count < format->min_field_count)) {
 		diag_set(ClientError, ER_INDEX_FIELD_COUNT,
 			 (unsigned) field_count,
-			 (unsigned) format->field_count);
+			 (unsigned) format->min_field_count);
 		return -1;
 	}
 
@@ -469,7 +472,9 @@ tuple_init_field_map(const struct tuple_format *format, uint32_t *field_map,
 	mp_next(&pos);
 	/* other fields...*/
 	++field;
-	for (uint32_t i = 1; i < format->field_count; i++, ++field) {
+	uint32_t i = 1;
+	uint32_t defined_field_count = MIN(field_count, format->field_count);
+	for (; i < defined_field_count; ++i, ++field) {
 		mp_type = mp_typeof(*pos);
 		if (key_mp_type_validate(field->type, mp_type, ER_FIELD_TYPE,
 					 i + TUPLE_INDEX_BASE,
diff --git a/src/box/tuple_format.h b/src/box/tuple_format.h
index f915f80734c680740e0dc3f96ef8014343170d82..bb92e14bbc739c3536fca70f7d9b516bf0ed1248 100644
--- a/src/box/tuple_format.h
+++ b/src/box/tuple_format.h
@@ -137,6 +137,11 @@ struct tuple_format {
 	 * element is used by an index.
 	 */
 	uint32_t index_field_count;
+	/**
+	 * The minimal field count that must be specified.
+	 * index_field_count <= min_field_count <= field_count.
+	 */
+	uint32_t min_field_count;
 	/* Length of 'fields' array. */
 	uint32_t field_count;
 	/** Field names hash. Key - name, value - field number. */
diff --git a/src/box/vy_stmt.c b/src/box/vy_stmt.c
index ea28c77c2ce974ec16bfae510c4f7a2210950ae7..df545ac3be01bd49199de996128104006686732b 100644
--- a/src/box/vy_stmt.c
+++ b/src/box/vy_stmt.c
@@ -246,7 +246,7 @@ vy_stmt_new_with_ops(struct tuple_format *format, const char *tuple_begin,
 
 	const char *tmp = tuple_begin;
 	uint32_t field_count = mp_decode_array(&tmp);
-	assert(field_count >= format->field_count);
+	assert(field_count >= format->min_field_count);
 	(void) field_count;
 
 	size_t ops_size = 0;
diff --git a/test/box/alter.result b/test/box/alter.result
index 88808d4fc0f2aca162ef2a43cdf3b80d6e9d6ef4..0b8ffe90fed298fa0378ce042261fa7f0563e856 100644
--- a/test/box/alter.result
+++ b/test/box/alter.result
@@ -984,11 +984,11 @@ s:replace{1, '2', {3, 3}, 4.4, -5, true, {7}, 8, 9}
 ...
 s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}}
 ---
-- error: Tuple field count 7 is less than required by a defined index (expected 9)
+- error: Tuple field count 7 is less than required (expected at least 9)
 ...
 s:replace{1, '2', {3, 3}, 4.4, -5, true, {value=7}, 8}
 ---
-- error: Tuple field count 8 is less than required by a defined index (expected 9)
+- error: Tuple field count 8 is less than required (expected at least 9)
 ...
 s:truncate()
 ---
@@ -1478,7 +1478,7 @@ s:format(format)
 -- Fail, not enough fields.
 s:replace{2, 2, 2, 2, 2}
 ---
-- error: Tuple field count 5 is less than required by a defined index (expected 6)
+- error: Tuple field count 5 is less than required (expected at least 6)
 ...
 s:replace{2, 2, 2, 2, 2, 2, 2}
 ---
@@ -1490,7 +1490,7 @@ format[7] = {name = 'field7', type = 'unsigned'}
 -- Fail, the tuple {1, ... 1} is invalid for a new format.
 s:format(format)
 ---
-- error: Tuple field count 6 is less than required by a defined index (expected 7)
+- error: Tuple field count 6 is less than required (expected at least 7)
 ...
 s:drop()
 ---
diff --git a/test/box/alter_limits.result b/test/box/alter_limits.result
index cd9a28945537e8a01ef935e93a8e09c9d95d551d..5252cd9854203692eeb53226ee8a61d533245972 100644
--- a/test/box/alter_limits.result
+++ b/test/box/alter_limits.result
@@ -845,7 +845,7 @@ index = s:create_index('string', { type = 'tree', unique =  false, parts = { 2,
 -- create index on a non-existing field
 index = s:create_index('nosuchfield', { type = 'tree', unique = true, parts = { 3, 'string'}})
 ---
-- error: Tuple field count 2 is less than required by a defined index (expected 3)
+- error: Tuple field count 2 is less than required (expected at least 3)
 ...
 s.index.year:drop()
 ---
@@ -866,7 +866,7 @@ s:replace{'Der Baader Meinhof Komplex'}
 ...
 index = s:create_index('year', { type = 'tree', unique = false, parts = { 2, 'unsigned'}})
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 s:drop()
 ---
diff --git a/test/box/ddl.result b/test/box/ddl.result
index 3f086f9f67174e53e2d80376048c9cea0f837e66..20808c32c3e0951ac35c1d3aa1e17d7599564cc4 100644
--- a/test/box/ddl.result
+++ b/test/box/ddl.result
@@ -313,27 +313,27 @@ box.internal.collation.drop('test')
 ...
 box.space._collation:auto_increment{'test'}
 ---
-- error: Tuple field count 2 is less than required by a defined index (expected 6)
+- error: Tuple field count 2 is less than required (expected at least 6)
 ...
 box.space._collation:auto_increment{'test', 0, 'ICU'}
 ---
-- error: Tuple field count 4 is less than required by a defined index (expected 6)
+- error: Tuple field count 4 is less than required (expected at least 6)
 ...
 box.space._collation:auto_increment{'test', 'ADMIN', 'ICU', 'ru_RU'}
 ---
-- error: Tuple field count 5 is less than required by a defined index (expected 6)
+- error: Tuple field count 5 is less than required (expected at least 6)
 ...
 box.space._collation:auto_increment{42, 0, 'ICU', 'ru_RU'}
 ---
-- error: Tuple field count 5 is less than required by a defined index (expected 6)
+- error: Tuple field count 5 is less than required (expected at least 6)
 ...
 box.space._collation:auto_increment{'test', 0, 42, 'ru_RU'}
 ---
-- error: Tuple field count 5 is less than required by a defined index (expected 6)
+- error: Tuple field count 5 is less than required (expected at least 6)
 ...
 box.space._collation:auto_increment{'test', 0, 'ICU', 42}
 ---
-- error: Tuple field count 5 is less than required by a defined index (expected 6)
+- error: Tuple field count 5 is less than required (expected at least 6)
 ...
 box.space._collation:auto_increment{'test', 0, 'ICU', 'ru_RU', setmap{}} --ok
 ---
diff --git a/test/box/sql.result b/test/box/sql.result
index 3c4a11913ccdca1dde93f514568cd776621ca7a0..a82f6c68d3c71c05ba41dcbce709a9537e040d1a 100644
--- a/test/box/sql.result
+++ b/test/box/sql.result
@@ -296,7 +296,7 @@ s:truncate()
 -- get away with it.
 space:insert{'Britney'}
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 sorted(space.index.secondary:select('Anything'))
 ---
@@ -304,7 +304,7 @@ sorted(space.index.secondary:select('Anything'))
 ...
 space:insert{'Stephanie'}
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 sorted(space.index.secondary:select('Anything'))
 ---
@@ -633,7 +633,7 @@ sorted(space.index.secondary:select('Britney'))
 -- try to insert the incoplete tuple
 space:replace{'Spears'}
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 -- check that nothing has been updated
 space:select{'Spears'}
diff --git a/test/box/tree_pk_multipart.result b/test/box/tree_pk_multipart.result
index d248eac7f114c5cf4697a8980d08130242918d83..054ceed69aff1033d511b0cb7a7da299449130dc 100644
--- a/test/box/tree_pk_multipart.result
+++ b/test/box/tree_pk_multipart.result
@@ -490,11 +490,11 @@ i1 = space:create_index('primary', { type = 'tree', parts = {1, 'unsigned', 3, '
 ...
 space:insert{1, 1}
 ---
-- error: Tuple field count 2 is less than required by a defined index (expected 3)
+- error: Tuple field count 2 is less than required (expected at least 3)
 ...
 space:replace{1, 1}
 ---
-- error: Tuple field count 2 is less than required by a defined index (expected 3)
+- error: Tuple field count 2 is less than required (expected at least 3)
 ...
 space:drop()
 ---
diff --git a/test/engine/ddl.result b/test/engine/ddl.result
index 5e3b43791629230104f025674d5d0b66bc113908..46bdff261f008c56dc9534ad800abdec04447f33 100644
--- a/test/engine/ddl.result
+++ b/test/engine/ddl.result
@@ -77,7 +77,7 @@ index = space:create_index('primary', {type = 'tree', parts = {1, 'unsigned', 2,
 ...
 space:insert({13})
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 space:drop()
 ---
diff --git a/test/engine/null.result b/test/engine/null.result
index 0b78625b772225ceccde4560972e863f7166c4f4..dfe89b69c043b51eb19227b1cc1354f10213fa52 100644
--- a/test/engine/null.result
+++ b/test/engine/null.result
@@ -455,3 +455,162 @@ sk2:select{}
 s:drop()
 ---
 ...
+--
+-- gh-2880: allow to store less field count than specified in a
+-- format.
+--
+format = {}
+---
+...
+format[1] = {name = 'field1', type = 'unsigned'}
+---
+...
+format[2] = {name = 'field2', type = 'unsigned'}
+---
+...
+format[3] = {name = 'field3'}
+---
+...
+format[4] = {name = 'field4', is_nullable = true}
+---
+...
+s = box.schema.create_space('test', {engine = engine, format = format})
+---
+...
+pk = s:create_index('pk')
+---
+...
+sk = s:create_index('sk', {parts = {2, 'unsigned'}})
+---
+...
+s:replace{1, 2} -- error
+---
+- error: Tuple field count 2 is less than required (expected at least 3)
+...
+t1 = s:replace{2, 3, 4}
+---
+...
+t2 = s:replace{3, 4, 5, 6}
+---
+...
+t1.field1, t1.field2, t1.field3, t1.field4
+---
+- 2
+- 3
+- 4
+- null
+...
+t2.field1, t2.field2, t2.field3, t2.field4
+---
+- 3
+- 4
+- 5
+- 6
+...
+ -- Ensure the tuple is read ok from disk in a case of vinyl.
+---
+...
+if engine == 'vinyl' then box.snapshot() end
+---
+...
+s:select{2}
+---
+- - [2, 3, 4]
+...
+s:drop()
+---
+...
+-- Check the case when not contiguous format tail is nullable.
+format = {}
+---
+...
+format[1] = {name = 'field1', type = 'unsigned'}
+---
+...
+format[2] = {name = 'field2', type = 'unsigned'}
+---
+...
+format[3] = {name = 'field3'}
+---
+...
+format[4] = {name = 'field4', is_nullable = true}
+---
+...
+format[5] = {name = 'field5'}
+---
+...
+format[6] = {name = 'field6', is_nullable = true}
+---
+...
+format[7] = {name = 'field7', is_nullable = true}
+---
+...
+s = box.schema.create_space('test', {engine = engine, format = format})
+---
+...
+pk = s:create_index('pk')
+---
+...
+sk = s:create_index('sk', {parts = {2, 'unsigned'}})
+---
+...
+s:replace{1, 2} -- error
+---
+- error: Tuple field count 2 is less than required (expected at least 5)
+...
+s:replace{2, 3, 4} -- error
+---
+- error: Tuple field count 3 is less than required (expected at least 5)
+...
+s:replace{3, 4, 5, 6} -- error
+---
+- error: Tuple field count 4 is less than required (expected at least 5)
+...
+t1 = s:replace{4, 5, 6, 7, 8}
+---
+...
+t2 = s:replace{5, 6, 7, 8, 9, 10}
+---
+...
+t3 = s:replace{6, 7, 8, 9, 10, 11, 12}
+---
+...
+t1.field1, t1.field2, t1.field3, t1.field4, t1.field5, t1.field6, t1.field7
+---
+- 4
+- 5
+- 6
+- 7
+- 8
+- null
+- null
+...
+t2.field1, t2.field2, t2.field3, t2.field4, t2.field5, t2.field6, t2.field7
+---
+- 5
+- 6
+- 7
+- 8
+- 9
+- 10
+- null
+...
+t3.field1, t3.field2, t3.field3, t3.field4, t3.field5, t3.field6, t3.field7
+---
+- 6
+- 7
+- 8
+- 9
+- 10
+- 11
+- 12
+...
+s:select{}
+---
+- - [4, 5, 6, 7, 8]
+  - [5, 6, 7, 8, 9, 10]
+  - [6, 7, 8, 9, 10, 11, 12]
+...
+s:drop()
+---
+...
diff --git a/test/engine/null.test.lua b/test/engine/null.test.lua
index 2755a8fc653e57871a6a45010c28f458ca95a851..7539d3bd6233267ebc02a5903e0f7fbe6dd0db5b 100644
--- a/test/engine/null.test.lua
+++ b/test/engine/null.test.lua
@@ -155,3 +155,53 @@ sk:select{}
 sk2:select{}
 
 s:drop()
+
+--
+-- gh-2880: allow to store less field count than specified in a
+-- format.
+--
+format = {}
+format[1] = {name = 'field1', type = 'unsigned'}
+format[2] = {name = 'field2', type = 'unsigned'}
+format[3] = {name = 'field3'}
+format[4] = {name = 'field4', is_nullable = true}
+s = box.schema.create_space('test', {engine = engine, format = format})
+pk = s:create_index('pk')
+sk = s:create_index('sk', {parts = {2, 'unsigned'}})
+
+s:replace{1, 2} -- error
+t1 = s:replace{2, 3, 4}
+t2 = s:replace{3, 4, 5, 6}
+t1.field1, t1.field2, t1.field3, t1.field4
+t2.field1, t2.field2, t2.field3, t2.field4
+ -- Ensure the tuple is read ok from disk in a case of vinyl.
+if engine == 'vinyl' then box.snapshot() end
+s:select{2}
+
+s:drop()
+
+-- Check the case when not contiguous format tail is nullable.
+format = {}
+format[1] = {name = 'field1', type = 'unsigned'}
+format[2] = {name = 'field2', type = 'unsigned'}
+format[3] = {name = 'field3'}
+format[4] = {name = 'field4', is_nullable = true}
+format[5] = {name = 'field5'}
+format[6] = {name = 'field6', is_nullable = true}
+format[7] = {name = 'field7', is_nullable = true}
+s = box.schema.create_space('test', {engine = engine, format = format})
+pk = s:create_index('pk')
+sk = s:create_index('sk', {parts = {2, 'unsigned'}})
+
+s:replace{1, 2} -- error
+s:replace{2, 3, 4} -- error
+s:replace{3, 4, 5, 6} -- error
+t1 = s:replace{4, 5, 6, 7, 8}
+t2 = s:replace{5, 6, 7, 8, 9, 10}
+t3 = s:replace{6, 7, 8, 9, 10, 11, 12}
+t1.field1, t1.field2, t1.field3, t1.field4, t1.field5, t1.field6, t1.field7
+t2.field1, t2.field2, t2.field3, t2.field4, t2.field5, t2.field6, t2.field7
+t3.field1, t3.field2, t3.field3, t3.field4, t3.field5, t3.field6, t3.field7
+s:select{}
+
+s:drop()
diff --git a/test/vinyl/constraint.result b/test/vinyl/constraint.result
index 0eda24ce07eebdc0ffd4bf09502e80bf1c9ecd4d..18b529691c615f5bb0c105eaca319f2fb24acbc8 100644
--- a/test/vinyl/constraint.result
+++ b/test/vinyl/constraint.result
@@ -83,11 +83,11 @@ index = space:create_index('primary', { type = 'tree', parts = {1,'unsigned',2,'
 ...
 space:insert{1}
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 space:replace{1}
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 space:delete{1}
 ---
@@ -99,7 +99,7 @@ space:update(1, {{'=', 1, 101}})
 ...
 space:upsert({1}, {{'+', 1, 10}})
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 2)
+- error: Tuple field count 1 is less than required (expected at least 2)
 ...
 space:get{1}
 ---
diff --git a/test/vinyl/savepoint.result b/test/vinyl/savepoint.result
index ad3c5c1318e5065ce80ce8c693229a2d0d9d78df..c233913f7d6b2592bb5e247af62c4fc4b52069f4 100644
--- a/test/vinyl/savepoint.result
+++ b/test/vinyl/savepoint.result
@@ -124,7 +124,7 @@ index2 = space:create_index('secondary', { parts = {2, 'int', 3, 'str'} })
 ...
 space:insert({1})
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 3)
+- error: Tuple field count 1 is less than required (expected at least 3)
 ...
 space:insert({1, 1, 'a'})
 ---
@@ -622,7 +622,7 @@ space:insert({4, 2, 'b'})
 ...
 space:upsert({2}, {{'=', 4, 1000}})
 ---
-- error: Tuple field count 1 is less than required by a defined index (expected 3)
+- error: Tuple field count 1 is less than required (expected at least 3)
 ...
 index3:delete({3, 'a'})
 ---