Repository: getmoto/moto

Issue:
DynamoDB get_item should fail if ExpressionAttributeNames is empty or unncecessary
Hi! I’d like to report a difference in behavior between Moto and the real DynamoDB.

A call like

```python
table = boto3.resource("dynamodb").Table("my-table")
table.get_item(
    Key={"id": my_id},
    ExpressionAttributeNames={},
)
```

will be accepted by Moto 2.2.3, but AWS DynamoDB will return `botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the GetItem operation: ExpressionAttributeNames can only be specified when using expressions` instead.

Therefore I think Moto should reject a call like that too.

**Edit:** After opening this issue, I’ve noticed that an _empty_ `ExpressionAttributeNames` (i.e. `{}`) is not accepted by DynamoDB either, even if there is a `ProjectionExpression`.

Why was I passing an empty dict at all, you ask? Because I have wrapped the `get_item` call in a convenience method in my application that did something like this:

```python
def _get_foo(self, id: str, proj_expr: List[str] = [], expr_attr_names: dict = {}) -> Dict[str, Any]:
    """Retrieve a single foo as a dict."""
    kwargs = {}
    if len(proj_expr):
        kwargs["ProjectionExpression"] = ", ".join(proj_expr)
    res = self._table("foos").get_item(Key={"id": id}, ExpressionAttributeNames=expr_attr_names, **kwargs)
```

I have fixed my method now by putting `ExpressionAttributeNames` in the `kwargs` in the `if` block instead and checking whether it’s not empty.


Discussion:
Thanks for raising this @scy - will mark it as an enhancement to implement validation here.

Resolving patch:
diff --git a/moto/dynamodb2/responses.py b/moto/dynamodb2/responses.py
--- a/moto/dynamodb2/responses.py
+++ b/moto/dynamodb2/responses.py
@@ -410,8 +410,19 @@ def get_item(self):
             )
         key = self.body["Key"]
         projection_expression = self.body.get("ProjectionExpression")
-        expression_attribute_names = self.body.get("ExpressionAttributeNames", {})
+        expression_attribute_names = self.body.get("ExpressionAttributeNames")
+        if expression_attribute_names == {}:
+            if projection_expression is None:
+                er = "ValidationException"
+                return self.error(
+                    er,
+                    "ExpressionAttributeNames can only be specified when using expressions",
+                )
+            else:
+                er = "ValidationException"
+                return self.error(er, "ExpressionAttributeNames must not be empty")
 
+        expression_attribute_names = expression_attribute_names or {}
         projection_expression = self._adjust_projection_expression(
             projection_expression, expression_attribute_names
         )


Test patch:
diff --git a/tests/test_dynamodb2/test_dynamodb_exceptions.py b/tests/test_dynamodb2/test_dynamodb_exceptions.py
--- a/tests/test_dynamodb2/test_dynamodb_exceptions.py
+++ b/tests/test_dynamodb2/test_dynamodb_exceptions.py
@@ -6,27 +6,28 @@
 from moto import mock_dynamodb2
 
 
+table_schema = {
+    "KeySchema": [{"AttributeName": "partitionKey", "KeyType": "HASH"}],
+    "GlobalSecondaryIndexes": [
+        {
+            "IndexName": "GSI-K1",
+            "KeySchema": [
+                {"AttributeName": "gsiK1PartitionKey", "KeyType": "HASH"},
+                {"AttributeName": "gsiK1SortKey", "KeyType": "RANGE"},
+            ],
+            "Projection": {"ProjectionType": "KEYS_ONLY",},
+        }
+    ],
+    "AttributeDefinitions": [
+        {"AttributeName": "partitionKey", "AttributeType": "S"},
+        {"AttributeName": "gsiK1PartitionKey", "AttributeType": "S"},
+        {"AttributeName": "gsiK1SortKey", "AttributeType": "S"},
+    ],
+}
+
+
 @mock_dynamodb2
 def test_query_gsi_with_wrong_key_attribute_names_throws_exception():
-    table_schema = {
-        "KeySchema": [{"AttributeName": "partitionKey", "KeyType": "HASH"}],
-        "GlobalSecondaryIndexes": [
-            {
-                "IndexName": "GSI-K1",
-                "KeySchema": [
-                    {"AttributeName": "gsiK1PartitionKey", "KeyType": "HASH"},
-                    {"AttributeName": "gsiK1SortKey", "KeyType": "RANGE"},
-                ],
-                "Projection": {"ProjectionType": "KEYS_ONLY",},
-            }
-        ],
-        "AttributeDefinitions": [
-            {"AttributeName": "partitionKey", "AttributeType": "S"},
-            {"AttributeName": "gsiK1PartitionKey", "AttributeType": "S"},
-            {"AttributeName": "gsiK1SortKey", "AttributeType": "S"},
-        ],
-    }
-
     item = {
         "partitionKey": "pk-1",
         "gsiK1PartitionKey": "gsi-pk",
@@ -96,3 +97,53 @@ def test_query_gsi_with_wrong_key_attribute_names_throws_exception():
     err["Message"].should.equal(
         "Query condition missed key schema element: gsiK1SortKey"
     )
+
+
+@mock_dynamodb2
+def test_empty_expressionattributenames():
+    ddb = boto3.resource("dynamodb", region_name="us-east-1")
+    ddb.create_table(
+        TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
+    )
+    table = ddb.Table("test-table")
+    with pytest.raises(ClientError) as exc:
+        table.get_item(
+            Key={"id": "my_id"}, ExpressionAttributeNames={},
+        )
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("ValidationException")
+    err["Message"].should.equal(
+        "ExpressionAttributeNames can only be specified when using expressions"
+    )
+
+
+@mock_dynamodb2
+def test_empty_expressionattributenames_with_empty_projection():
+    ddb = boto3.resource("dynamodb", region_name="us-east-1")
+    ddb.create_table(
+        TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
+    )
+    table = ddb.Table("test-table")
+    with pytest.raises(ClientError) as exc:
+        table.get_item(
+            Key={"id": "my_id"}, ProjectionExpression="", ExpressionAttributeNames={},
+        )
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("ValidationException")
+    err["Message"].should.equal("ExpressionAttributeNames must not be empty")
+
+
+@mock_dynamodb2
+def test_empty_expressionattributenames_with_projection():
+    ddb = boto3.resource("dynamodb", region_name="us-east-1")
+    ddb.create_table(
+        TableName="test-table", BillingMode="PAY_PER_REQUEST", **table_schema
+    )
+    table = ddb.Table("test-table")
+    with pytest.raises(ClientError) as exc:
+        table.get_item(
+            Key={"id": "my_id"}, ProjectionExpression="id", ExpressionAttributeNames={},
+        )
+    err = exc.value.response["Error"]
+    err["Code"].should.equal("ValidationException")
+    err["Message"].should.equal("ExpressionAttributeNames must not be empty")