diff --git a/moto/dynamodb/exceptions.py b/moto/dynamodb/exceptions.py index 9f572764f..27d398a9c 100644 --- a/moto/dynamodb/exceptions.py +++ b/moto/dynamodb/exceptions.py @@ -332,3 +332,8 @@ class TransactWriteSingleOpException(MockValidationException): def __init__(self): super().__init__(self.there_can_be_only_one) + + +class SerializationException(DynamodbException): + def __init__(self, msg): + super().__init__(error_type="SerializationException", message=msg) diff --git a/moto/dynamodb/models/__init__.py b/moto/dynamodb/models/__init__.py index febe9f3c5..5345ebc8d 100644 --- a/moto/dynamodb/models/__init__.py +++ b/moto/dynamodb/models/__init__.py @@ -33,6 +33,7 @@ from moto.dynamodb.exceptions import ( MockValidationException, InvalidConversion, TransactWriteSingleOpException, + SerializationException, ) from moto.dynamodb.models.utilities import bytesize from moto.dynamodb.models.dynamo_type import DynamoType @@ -658,6 +659,17 @@ class Table(CloudFormationModel): self._validate_item_types(value) elif type(value) == int and key == "N": raise InvalidConversion + if key == "S": + # This scenario is usually caught by boto3, but the user can disable parameter validation + # Which is why we need to catch it 'server-side' as well + if type(value) == int: + raise SerializationException( + "NUMBER_VALUE cannot be converted to String" + ) + if type(value) == dict: + raise SerializationException( + "Start of structure or map found where not expected" + ) def put_item( self, @@ -699,9 +711,8 @@ class Table(CloudFormationModel): actual_type=range_value.type, ) - self._validate_key_sizes(item_attrs) - self._validate_item_types(item_attrs) + self._validate_key_sizes(item_attrs) if expected is None: expected = {} diff --git a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py index 40767e466..069a2ba58 100644 --- a/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py +++ b/tests/test_dynamodb/exceptions/test_dynamodb_exceptions.py @@ -870,3 +870,29 @@ def test_update_primary_key(): table.get_item(Key={"pk": "testchangepk"})["Item"].should.equal( {"pk": "testchangepk"} ) + + +@mock_dynamodb +def test_put_item__string_as_integer_value(): + if settings.TEST_SERVER_MODE: + raise SkipTest("Unable to mock a session with Config in ServerMode") + session = botocore.session.Session() + config = botocore.client.Config(parameter_validation=False) + client = session.create_client("dynamodb", region_name="us-east-1", config=config) + client.create_table( + TableName="without_sk", + KeySchema=[{"AttributeName": "pk", "KeyType": "HASH"}], + AttributeDefinitions=[{"AttributeName": "pk", "AttributeType": "S"}], + ProvisionedThroughput={"ReadCapacityUnits": 10, "WriteCapacityUnits": 10}, + ) + with pytest.raises(ClientError) as exc: + client.put_item(TableName="without_sk", Item={"pk": {"S": 123}}) + err = exc.value.response["Error"] + err["Code"].should.equal("SerializationException") + err["Message"].should.equal("NUMBER_VALUE cannot be converted to String") + + with pytest.raises(ClientError) as exc: + client.put_item(TableName="without_sk", Item={"pk": {"S": {"S": "asdf"}}}) + err = exc.value.response["Error"] + err["Code"].should.equal("SerializationException") + err["Message"].should.equal("Start of structure or map found where not expected")