DynamoDB "One or More Parameter Values Were Invalid": Every Message After the Colon Decoded (Missing the Key, Type Mismatch, Index Key, Empty Set)

Logeshwaran
—

"One or more parameter values were invalid" is DynamoDB's way of saying "I refused this request before touching your data," and the first half of the message tells you almost nothing. The diagnosis is in the words after the colon. "Missing the key id in the item" means the request had no partition key at all. "Type mismatch for key id expected: S actual: N" means you sent a number where the table expects a string. "Type mismatch for Index Key" means an index, not the table, rejected the value. "An string set may not be empty" means a set with nothing in it. "Condition parameter type does not match schema type" means a query compared a key with the wrong type. Every one of them is a 400 ValidationException, nothing was written, and retrying won't help. Here is the surprise behind the most common one: in a Lambda function behind API Gateway, event.id is undefined, and the AWS SDK for JavaScript quietly drops undefined attributes before the request leaves, so DynamoDB never sees an id and says it's missing, even though your code plainly wrote id: event.id. This page decodes every message that can follow the colon, shows the one-line log that proves what you really sent, and gives the fix for each in Python, Node.js, the CLI, CloudFormation and Terraform.

Jake runs a phone repair shop, and his customers book repairs through a small form on his website. His friend Ethan, a developer, wired the form to a Lambda function that saves each booking in a DynamoDB table. It worked on Ethan's laptop. The first real customer got a polite "something went wrong," and the logs said One or more parameter values were invalid: Missing the key id in the item. Ethan fixed that one, and a week later added an index so Jake could list bookings by status. Then a handful of old bookings refused every update, with a different message about the index. This page is what Ethan explained over the counter: how to read the message, why each variant happens, the Lambda trap, the index trap, empty strings and sets, the CreateTable rules, the old-style parameters, the size limits, why DynamoDB Local words things differently, and how to see the exact request you sent.

⚡ Quick Answer

• Read the words after the colon. They name the exact problem; the decoder table maps each one to its fix.

• "Missing the key id in the item" → the key never reached DynamoDB, usually because the value was undefined in a Lambda event. The Lambda trap.

• "Type mismatch for key id expected: S actual: N" (or M, NULL, L) → wrong type for the key; cast it, or stop wrapping values twice. Type mismatch.

• "Type mismatch for Index Key" → a global secondary index key has the wrong type; old items can become impossible to update. The index trap.

• CreateTable or CloudFormation errors about AttributeDefinitions → list only key attributes there. The rule.

Not sure what you sent? Log the request in one line and read it.

New to DynamoDB? Our plain-English guide to DynamoDB explains tables, keys and items in about ten minutes. You can follow this page without it; every step starts from zero.

🧭 NEW HERE? READ THESE FIRST

New to DynamoDB, Lambda or boto3? These five pages make the rest of this one easy:

📌 Bookmark this; the decoder table and the request-logging lines are the parts you will come back for.

What "One or more parameter values were invalid" means: read the words after the colon

Every request you send to DynamoDB goes through a validation step before it reaches your data. DynamoDB checks that the key attributes are present, that their types match the table and its indexes, that values are inside the size limits, and that the parameters make sense together. If any check fails, the request stops right there and you get HTTP 400 with the error code ValidationException. Nothing is written, nothing is read, and the request isn't throttled or retried for you, because a malformed request will fail the same way every time.

The message always starts the same way, and then names what failed. In Python it looks like this:

botocore.exceptions.ClientError: An error occurred (ValidationException) when calling the PutItem
operation: One or more parameter values were invalid: Missing the key id in the item

In Node.js the error's name is ValidationException and its message is the text after "operation:". In Java the same text is followed by something like (Service: DynamoDb, Status Code: 400, Request ID: ...). The part that matters is identical everywhere. Here is every common ending, what it means, and where the fix is on this page:

After the colonWhat it meansUsual causeFix
Missing the key id in the itemThe item has no partition key (or sort key) attributeValue was undefined, misspelled, or wrong caseMissing key
Type mismatch for key id expected: S actual: NKey attribute has the wrong typeNumber sent for a string key, or the reverseType mismatch
... expected: S actual: MA map was sent as the keyA whole object, or a value wrapped twiceType mismatch
... expected: S actual: NULLA null was sent as the keyPython None, JavaScript null, or convertEmptyValuesType mismatch
Type mismatch for Index Key X Expected: S Actual: N IndexName: YA global secondary index key has the wrong typeAn attribute that became an index key laterIndex trap
Schema violation against backfilling indexSame index problem while a new index is still being builtWriting a bad index value during index creationIndex trap
(are not valid.) The AttributeValue for a key attribute cannot contain an empty string value. Key: idEmpty string in a keyBlank form field used as the keyEmpty strings
An string set may not be emptyA set attribute with no membersEmpty array or set() sent as a setSets
Input collection [a, a] contains duplicatesA set with repeated membersA list converted to a set without removing repeatsSets
Condition parameter type does not match schema typeA Query compared a key with the wrong typeA number arriving as text from a URLQuery types
Number of attributes in KeySchema does not exactly match number of attributes defined in AttributeDefinitionsCreateTable lists extra attributesNon-key attributes in AttributeDefinitionsCreateTable
Some index key attributes are not defined in AttributeDefinitionsAn index key isn't declaredForgot to add the index key's typeCreateTable
Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUESTThroughput set on an on-demand tableLeftover ProvisionedThroughput in a templateCreateTable
Null attribute value types must have the value of true{"NULL": false} was sentHand-written JSONUpdates
Value must be provided when Exists is true for Attribute: XOld-style Expected parameter misusedLegacy code or libraryOld parameters
ComparisonOperator CONTAINS is not valid for SS AttributeValue typeOld-style filter given the wrong value typeLegacy ScanFilter or QueryFilterOld parameters
Size of hashkey has exceeded the maximum size limitPartition key over 2,048 bytesLong text or a whole document used as a keyLimits

Two close cousins are not on this list because they have their own pages. "The provided key element does not match the schema" usually comes from GetItem, UpdateItem and DeleteItem with a wrong Key; our guide to the key element error covers it. "Item size has exceeded the maximum allowed size" is the 400 KB limit, covered in our item size guide.

Jake: "So the long sentence is just the envelope, and the real letter is the bit after the colon."

Ethan: "Exactly. DynamoDB is a strict receptionist. It checks the form before it takes it to the back room, and the bit after the colon is the box you left empty or filled in wrong."

"Missing the key id in the item": the key never reached DynamoDB

A DynamoDB table has a primary key: a partition key, and sometimes a sort key too. Every item you write with PutItem must carry those attributes, with exactly the names and types the table was created with. "Missing the key id in the item" means DynamoDB looked for an attribute called id in the item you sent and didn't find one. The name in the message is your table's key name, so you'll see variants such as "Missing the key PK in the item", "Missing the key LockId in the item" (a Terraform lock table) or "Missing the key studentId in the item".

There are four reasons the key goes missing, roughly in order of how often they happen:

  1. The value was undefined, so the SDK removed it. The code wrote id: event.id or id: body.id, the value didn't exist, and the attribute vanished before the request was sent. This is the Lambda trap, and it has its own section.
  2. The name doesn't match exactly. Key names are case-sensitive: a table keyed on id rejects an item with Id or ID, and a table keyed on userId rejects user_id. The attribute is in your item, under a name the table doesn't recognize, so the key counts as missing.
  3. The table has a sort key you forgot. With a composite key, both parts are required on every write. A table keyed on PK and SK rejects an item that has only PK, and the message names whichever key is absent.
  4. You're writing to a different table than you think. The same code pointed at a table created with another key name, in another account, Region or stage, fails this way on every write.

The first step is always the same: find out what the table's key really is. One CLI command shows the table's key, every index's key, and the declared types:

aws dynamodb describe-table --table-name Bookings \
  --query "Table.{Key: KeySchema, Types: AttributeDefinitions, Indexes: GlobalSecondaryIndexes[].KeySchema}"
{
    "Key": [ { "AttributeName": "id", "KeyType": "HASH" } ],
    "Types": [ { "AttributeName": "id", "AttributeType": "S" } ],
    "Indexes": null
}

Here the table needs an attribute named exactly id, of type S (string), on every item. If your item has that and you still get the message, the value isn't reaching the request, and the next step is to log what you actually sent.

In Python with boto3's Table resource, a correct write looks like this:

import boto3

table = boto3.resource("dynamodb").Table("Bookings")
table.put_item(Item={"id": "B-1042", "name": "Jake", "device": "iPhone 15"})

With the low-level client, every value carries its type letter:

client = boto3.client("dynamodb")
client.put_item(
    TableName="Bookings",
    Item={"id": {"S": "B-1042"}, "name": {"S": "Jake"}, "device": {"S": "iPhone 15"}},
)

Mixing those two styles up causes the next big family of errors, the type mismatches, so keep in mind which one your code uses.

The Lambda trap: event.id is undefined, and the SDK quietly drops it

This is the one that catches the most people, and it's why "Missing the key id in the item" is so confusing: your code clearly sets id. Here is a handler that looks right:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";

const doc = DynamoDBDocumentClient.from(new DynamoDBClient({}));

export const handler = async (event) => {
  await doc.send(new PutCommand({
    TableName: "Bookings",
    Item: { id: event.id, name: event.name },
  }));
  return { statusCode: 201, body: "booked" };
};

When you test it in the Lambda console with a test event of {"id": "B-1042", "name": "Jake"}, it works. When a real request arrives through API Gateway, it fails. The reason is the shape of the event. With a Lambda proxy integration, API Gateway doesn't hand your function the JSON the customer sent. It hands over a wrapper with the request details, and the customer's JSON sits inside event.body as a string:

{
  "httpMethod": "POST",
  "path": "/bookings",
  "headers": { "content-type": "application/json" },
  "body": "{\"id\":\"B-1042\",\"name\":\"Jake\"}",
  "isBase64Encoded": false
}

So event.id is undefined. Now the surprising part. The DynamoDB DocumentClient in the AWS SDK for JavaScript v3 (the @aws-sdk/lib-dynamodb package; the Node.js 22 Lambda runtime ships version 3.1105) drops a top-level attribute whose value is undefined and sends the rest. The request that leaves your function is this:

{"TableName":"Bookings","Item":{}}

An empty item: event.name was undefined too, so it went the same way. No id at all, so DynamoDB answers "Missing the key id in the item." Nothing in your code fails before that point, and nothing in the error mentions undefined. The behavior is inconsistent in a way that makes it harder to spot: an undefined value inside a nested map makes the DocumentClient throw "Pass options.removeUndefinedValues=true to remove undefined values from map/array/set." before anything is sent, but an undefined top-level attribute is simply left out.

The removeUndefinedValues: true option, which many answers recommend for that nested error, makes the top-level case no better: it removes undefined values everywhere, so a missing key still goes out missing, and you still get the same DynamoDB message. The option is useful for optional attributes. It never rescues a key.

The fix is to parse the body and refuse the request yourself when the key is absent, so the customer gets a clear 400 from you rather than a confusing one from DynamoDB:

export const handler = async (event) => {
  let input = event;
  if (typeof event.body === "string") {
    const raw = event.isBase64Encoded ? Buffer.from(event.body, "base64").toString("utf8") : event.body;
    input = JSON.parse(raw);
  }
  if (!input.id) {
    return { statusCode: 400, body: JSON.stringify({ message: "id is required" }) };
  }
  await doc.send(new PutCommand({
    TableName: "Bookings",
    Item: { id: String(input.id), name: input.name },
  }));
  return { statusCode: 201, body: JSON.stringify({ id: input.id }) };
};

String(input.id) also protects you from the type mismatch in the next section, when a client sends "id": 1042 as a number. The same handler in Python:

import base64, json
import boto3

table = boto3.resource("dynamodb").Table("Bookings")

def handler(event, context):
    body = event.get("body")
    if isinstance(body, str):
        if event.get("isBase64Encoded"):
            body = base64.b64decode(body).decode("utf-8")
        data = json.loads(body)
    else:
        data = event
    if not data.get("id"):
        return {"statusCode": 400, "body": json.dumps({"message": "id is required"})}
    table.put_item(Item={"id": str(data["id"]), "name": data.get("name")})
    return {"statusCode": 201, "body": json.dumps({"id": data["id"]})}

Python fails differently, which is worth knowing. If you write Item={"id": event.get("id")} and the value is missing, boto3 sends None as a DynamoDB NULL rather than dropping it, so the error becomes "Type mismatch for key id expected: S actual: NULL" instead of "Missing the key". Same bug, different words. If your function returns something API Gateway can't read after you fix this, our guide to the proxy response shape covers the 502 that follows.

Jake: "So the test button worked because Ethan typed the form in by hand, and the real website wraps it in an envelope."

Ethan: "Right. I was reading the address off the envelope instead of opening it. And the SDK, trying to be helpful, threw away the blank line where the booking number should have been."

"Type mismatch for key id expected: S actual: N" (and M, NULL, L)

DynamoDB is loose about most attributes, but strict about keys. When a table is created, each key attribute gets one of three types: S (string), N (number) or B (binary). Every item must use that type for that attribute, forever. The message tells you both sides: "expected" is the type the table was created with, and "actual" is the type you sent. The type letters you can see in the "actual" slot are DynamoDB's own data type names:

LetterTypeWhat usually sent it
SStringText
NNumberA number in your code, such as 1042 instead of "1042"
BBinaryBytes, such as an encrypted or hashed value
MMapAn object or dictionary, often a value wrapped twice
LListAn array
NULLNullPython None, JavaScript null, or an empty string converted to null

Each "actual" value points at its own mistake.

actual: N (number for a string key, or the reverse)

The table's key is a string and your code sent a number: {"id": 1042} instead of {"id": "1042"}. This happens when IDs come from a JSON body where the client sent a number, from an auto-increment counter, or from a spreadsheet import. The reverse, "expected: N actual: S", happens when a numeric key arrives as text from a URL or a form. Convert at the edge, once: String(id) in JavaScript, str(id) in Python, or Number(id) / Decimal(id) if the key is numeric. Changing the table's key type isn't possible after creation, so the code has to match the table.

actual: M (a map where a string should be)

This one has two classic causes. The first is passing a whole object as the key, for example Item={"id": event} or Key: { id: user }, where the variable holds an object rather than the ID inside it. The second, very common with boto3, is wrapping a value twice: using the Table resource, which converts plain Python values for you, but writing the value in the low-level typed format anyway:

table = boto3.resource("dynamodb").Table("Bookings")
table.put_item(Item={"id": {"S": "B-1042"}})     # wrong: the resource wraps it again

The resource sees a dictionary and dutifully stores it as a map, so the request carries {"id": {"M": {"S": {"S": "B-1042"}}}}, and DynamoDB reports "expected: S actual: M". The fix is to pick one style. With boto3.resource(...).Table(...), write plain values ({"id": "B-1042"}). With boto3.client("dynamodb"), write typed values ({"id": {"S": "B-1042"}}). The same applies in JavaScript: the DocumentClient (PutCommand) takes plain values, and the low-level client (PutItemCommand) takes typed ones.

actual: NULL

A null reached the key. In Python, None becomes a DynamoDB NULL. In JavaScript, null does the same, and so does an empty string when the DocumentClient's convertEmptyValues option is on (more on that in the empty string section). The fix is the same as for a missing key: validate the input and refuse the request before calling DynamoDB.

actual: L

An array arrived where the key belongs, usually because a form or API sent {"id": ["B-1042"]}, or because code collected IDs into a list and passed the list instead of one element. Take the single value you meant, or loop and write one item per ID.

The same "Type mismatch for key" message appears on GetItem, UpdateItem and DeleteItem when the Key parameter has the wrong type, and in a Query you get the differently worded "Condition parameter type does not match schema type", covered below.

"Type mismatch for Index Key", and items that suddenly can't be updated

A global secondary index (GSI) is a second way into the same table, keyed on different attributes. When you create one, its key attributes get types too, and from that moment DynamoDB enforces those types on every write to the table, not only on reads from the index. That's the source of this message, which names the index and both types:

One or more parameter values were invalid: Type mismatch for Index Key categoryParentsId
Expected: S Actual: L IndexName: gsi-Category.parents

If you write a new item whose index key attribute has the wrong type, the fix is the same as for a table key: send the right type. The more interesting case is what happens when you add an index to a table that already has data.

What happens when you add an index to a table with data

Creating a GSI on an existing table starts a backfill: DynamoDB reads every item and copies the eligible ones into the new index. An item is not eligible if its value for the index key has the wrong type (a status stored as a number when the index expects a string), is longer than an index key may be (2,048 bytes for a partition key, 1,024 for a sort key), or is an empty string. Those items aren't rejected and nothing reports them. The backfill simply skips them, finishes, and the index goes ACTIVE with fewer items than the table.

After the backfill, every write to one of those skipped items is checked against the new index key schema, and fails. That includes writes that have nothing to do with the index. Updating a customer's phone number on an old booking fails, because the item as a whole still carries an index key value the index can't accept. The message for that update reads:

The update expression attempted to update the secondary index key to unsupported type

And while the index is still being built, writes that break its key schema are refused with "Schema violation against backfilling index". The items are, in effect, read-only until you fix them. Two things still work:

  • Fixing the index attribute itself. An UpdateItem that sets the attribute to the correct type, such as SET GSI1PK = :v with :v as a string, succeeds, and the item then joins the index.
  • Removing it or deleting the item. REMOVE GSI1PK takes the item out of the index's concern entirely (an item without the attribute simply isn't indexed, which is how sparse indexes work), and DeleteItem always works.

So the repair job is: find every item whose index key attribute has the wrong type, size, or an empty value, and fix or remove that one attribute. The tool most guides point to for this, Violation Detector, was archived on June 18, 2020 and is now read-only. A short boto3 script does the same job:

import boto3

TABLE = "Bookings"
INDEX_KEYS = {"GSI1PK": "S"}          # index key attribute -> type it must be: "S", "N" or "B"
LIMITS = {"S": 2048, "B": 2048}       # partition key max bytes (use 1024 for a sort key)

client = boto3.client("dynamodb")
table_keys = [k["AttributeName"] for k in client.describe_table(TableName=TABLE)["Table"]["KeySchema"]]
names = table_keys + list(INDEX_KEYS)
paginator = client.get_paginator("scan")
for page in paginator.paginate(
    TableName=TABLE,
    ProjectionExpression=", ".join("#a" + str(i) for i in range(len(names))),
    ExpressionAttributeNames={"#a" + str(i): n for i, n in enumerate(names)},
):
    for item in page["Items"]:
        for name, want in INDEX_KEYS.items():
            if name not in item:
                continue                      # missing is fine: the item just stays out of the index
            got, value = next(iter(item[name].items()))
            size = len(value) if got == "B" else len(str(value).encode())
            problem = None
            if got != want:
                problem = f"type {got}, index wants {want}"
            elif got in ("S", "B") and size == 0:
                problem = "empty value"
            elif got in LIMITS and size > LIMITS[got]:
                problem = "too long for an index key"
            if problem:
                print({k: item[k] for k in table_keys}, name, problem)

It prints the primary key of every item the index rejected and why. The repair, in order:

  1. Run the script. The projection keeps each read small, but a scan still reads every item, so on a large provisioned table run it off-peak.
  2. For each line it prints, convert the attribute to the right type with an UpdateItem (SET GSI1PK = :v), or remove it (REMOVE GSI1PK) if the item shouldn't be in the index.
  3. Fix the code that wrote the bad values, so new items arrive with the right type.
  4. Run the script again until it prints nothing.

Before you create an index on a table with history, run the same script first, with the attributes and types you plan to use. It's much easier to clean a few dozen items before the index exists than to discover them one failed update at a time.

Jake: "So when Ethan added the 'list by status' index, the old bookings where somebody typed the status as a number just got left out, and then locked."

Ethan: "Locked against everything except fixing the status. It's like the new filing cabinet refusing a folder with the label on upside down, and the receptionist then refusing to put anything else in that folder until you turn the label around."

Empty strings: "The AttributeValue for a key attribute cannot contain an empty string value"

Empty strings have a history in DynamoDB, and the history explains why you'll find two different messages for them. Until May 18, 2020, DynamoDB refused empty String and Binary values everywhere, with "One or more parameter values were invalid: An AttributeValue may not contain an empty string". On that date it started accepting empty values for any attribute that isn't a key. Key attributes of the table, and key attributes of every local and global secondary index, still can't be empty.

So today an empty string fails only in a key, and the message has a slightly different opening:

One or more parameter values are not valid. The AttributeValue for a key attribute cannot
contain an empty string value. Key: id

Note "are not valid" instead of "were invalid": it's the same family, worded differently. For an index key the message adds the index name:

One or more parameter values are not valid. A value specified for a secondary index key is
not supported. The AttributeValue for a key attribute cannot contain an empty string value.
IndexName: gsi1, IndexKey: GSI1PK

If you still see the old "An AttributeValue may not contain an empty string" wording, whatever answered your request is older than May 2020, for example an old DynamoDB Local image or JAR, which gained the same support on May 21, 2020. Update it, and non-key empty strings will start working.

The fixes depend on which attribute is empty:

  • An empty table key means a blank form field became the ID. Refuse the request, or generate an ID yourself (crypto.randomUUID() in Node.js, uuid.uuid4() in Python).
  • An empty index key is usually an optional field you indexed. Leave the attribute out of the item when it's empty, instead of storing "". An item without the attribute is simply not in the index, which is usually what you want.
  • Any other attribute can be empty today. If it fails, you're on an old engine or an old library.

The convertEmptyValues trap

Older answers suggest turning on the DocumentClient's convertEmptyValues: true option to "fix" empty strings. For a key, it swaps one error for another. The option converts empty strings and empty sets to DynamoDB NULL, so an empty key goes out as {"id": {"NULL": true}}, and DynamoDB answers "Type mismatch for key id expected: S actual: NULL". For non-key attributes it stores a null where you had a blank, which changes what your reads get back. Since non-key empty strings have been allowed since 2020, the option is rarely what you want for strings. Leave it off, validate keys yourself, and omit optional index attributes when they're blank.

"An string set may not be empty" and "contains duplicates"

DynamoDB has three set types: string sets (SS), number sets (NS) and binary sets (BS). A set attribute must contain at least one member, and every member must be unique. Empty lists and maps are allowed; an empty set is not. Break the first rule and the message, with its famous grammar, reads "One or more parameter values were invalid: An string set may not be empty" (or "An number set may not be empty"). Break the second and you get "Input collection [screen, screen] contains duplicates".

Python has a twist that confuses people. boto3 converts a Python set() to a DynamoDB set by looking at its members. An empty set has no members to look at, so boto3 sends it as a number set, {"NS": []}, and the error says "An number set may not be empty" even when your set was meant to hold strings. JavaScript fails earlier: the DocumentClient refuses an empty Set before sending anything, with "Pass a non-empty set, or options.convertEmptyValues=true."

The fix is to never store an empty set:

  • When writing a whole item, leave the attribute out if the set is empty: if tags: item["tags"] = tags.
  • When removing members, use the DELETE action. When it removes the last member, DynamoDB removes the attribute for you, so the set never becomes empty:
table.update_item(
    Key={"id": "B-1042"},
    UpdateExpression="DELETE parts :done",
    ExpressionAttributeValues={":done": {"screen", "battery"}},
)
  • When replacing a set, use REMOVE parts instead of SET parts = :empty if the new value would be empty.
  • For duplicates, build the set from unique values: a Python set() or JavaScript new Set() removes repeats for you, while a list sent as a set (with the low-level client's "SS": [...]) keeps them and fails.

If you actually need an ordered collection that can repeat or be empty, use a list (L) instead of a set. Lists allow both.

The same checks run on the values you pass to an UpdateExpression or a condition, and then the message gains a prefix that names the placeholder: ExpressionAttributeValues contains invalid value: One or more parameter values were invalid: An string set may not be empty for key :t. Older code that set an attribute to "" before 2020 saw the empty-string version of the same prefix. Read it the same way: the part after the second colon is the problem, and :t tells you which placeholder carries it.

"Condition parameter type does not match schema type": a Query with the wrong type

This is the Query version of the type mismatch. A Query must name the partition key, and DynamoDB compares the value you give in KeyConditionExpression with the key's declared type. If the key is a string and your value is a number (or the reverse), you get:

One or more parameter values were invalid: Condition parameter type does not match schema type

Unlike the PutItem message, it doesn't name the attribute, so check each placeholder in ExpressionAttributeValues against the key types from describe-table. The most common cause is a number that arrives as text. Query string parameters, path parameters and form fields are always strings, so a table with a numeric key needs a conversion:

from boto3.dynamodb.conditions import Key
from decimal import Decimal

customer_id = event["pathParameters"]["customerId"]          # always a string, e.g. "1042"
resp = table.query(KeyConditionExpression=Key("customerId").eq(Decimal(customer_id)))

The reverse happens too: a string key queried with a number that came from a JSON body. The same message appears when you query a global secondary index with the wrong type for the index's key, and when old code uses the legacy KeyConditions parameter with a wrongly typed AttributeValueList. Our guide to filter expressions covers the related case where a filter silently matches nothing because of a number stored as a string; that one never raises an error at all.

CreateTable, CloudFormation and Terraform: the AttributeDefinitions rule

A whole group of these messages appears only when a table or index is created or changed, and almost all of them come from one misunderstanding about AttributeDefinitions. People read it as "the columns of my table." It isn't. DynamoDB has no columns; any item can carry any attributes. AttributeDefinitions declares the type of only the attributes used as keys: the table's partition and sort key, and the partition and sort key of each secondary index. Nothing else belongs there.

Break the rule in one direction, by listing an attribute that isn't a key, and the same mistake shows up as either of two messages:

One or more parameter values were invalid: Number of attributes in KeySchema does not exactly
match number of attributes defined in AttributeDefinitions

One or more parameter values were invalid: Some AttributeDefinitions are not used.

The second one goes on to list the attribute names you declared, which makes the extra ones easy to spot.

Break it in the other direction, by using a key that isn't declared, and you get "Some index key attributes are not defined in AttributeDefinitions". Here is a correct definition for a table keyed on id with an index on status and createdAt, in CloudFormation:

BookingsTable:
  Type: AWS::DynamoDB::Table
  Properties:
    TableName: Bookings
    BillingMode: PAY_PER_REQUEST
    AttributeDefinitions:            # keys only: table key + every index key
      - AttributeName: id
        AttributeType: S
      - AttributeName: status
        AttributeType: S
      - AttributeName: createdAt
        AttributeType: S
    KeySchema:
      - AttributeName: id
        KeyType: HASH
    GlobalSecondaryIndexes:
      - IndexName: byStatus
        KeySchema:
          - AttributeName: status
            KeyType: HASH
          - AttributeName: createdAt
            KeyType: RANGE
        Projection:
          ProjectionType: ALL

Customer name, phone and device aren't declared anywhere, and they don't need to be; every item can still carry them. Terraform enforces the same rule before it sends anything, with its own wording: "all attributes must be indexed. Unused attributes: ..." means an attribute block names something that isn't a table or index key. Delete that block. The AWS CDK generates the definitions from partitionKey and sortKey, so you rarely hit this there.

The two throughput messages

The other pair is about billing mode. An on-demand table (BillingMode: PAY_PER_REQUEST) has no provisioned capacity, so including ProvisionedThroughput fails with "Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST". It most often shows up on a CloudFormation stack update after someone switched the table to on-demand but left the ProvisionedThroughput block in the template, on the table or on an index. Remove every ProvisionedThroughput block, including the ones under GlobalSecondaryIndexes.

A provisioned table is the opposite: each index needs its own capacity, and an index without it fails with "ProvisionedThroughput must be specified for index: byStatus". Add a ProvisionedThroughput block to each index, or switch the table to on-demand. If a failed create leaves your stack stuck, our guide to ROLLBACK_COMPLETE shows how to recover it.

Jake: "So I don't have to list every box on the booking form when I make the table?"

Ethan: "Only the ones you'll look things up by. The table is a drawer of folders; you tell DynamoDB which label it files by, and each folder can hold whatever paper you like."

UpdateItem messages and the old-style parameters

A few messages come from UpdateItem and from parameters that DynamoDB kept for backward compatibility. They're rarer, but they're confusing when they appear, because modern examples never use the parameters involved.

Updating a key attribute

You can't change a key with UpdateItem. A SET id = :new on the table's partition or sort key is refused, because the key is the item's identity, not one of its fields. To "rename" an item, write a copy with the new key and delete the old one. A transaction keeps the two steps together, so you never end up with both or neither:

client.transact_write_items(TransactItems=[
    {"Put": {"TableName": "Bookings", "Item": new_item,
             "ConditionExpression": "attribute_not_exists(id)"}},
    {"Delete": {"TableName": "Bookings", "Key": {"id": {"S": "B-1042"}}}},
])

Index key attributes are different: you can update them freely, and the item moves within the index. That's exactly how you repair the read-only items from the index section.

"Null attribute value types must have the value of true"

In DynamoDB's typed format, a null is written {"NULL": true}. The value false isn't allowed. This message means something sent {"NULL": false}, usually hand-written JSON for the CLI or a converter that maps "not null" to false. To store a value, use its real type; to store nothing, leave the attribute out or REMOVE it.

The legacy parameters

Before expressions existed, DynamoDB used a set of older parameters, and some libraries and old code still send them. They produce their own messages:

Message after the colonOld parameterModern replacement
Value must be provided when Exists is true for Attribute: AssetLabelExpectedConditionExpression, e.g. attribute_exists(AssetLabel)
Invalid number of argument(s) for the CONTAINS ComparisonOperatorScanFilter / QueryFilterFilterExpression with contains(tags, :t)
ComparisonOperator CONTAINS is not valid for SS AttributeValue typeScanFilter / QueryFilterFilterExpression; compare against one member, not a set
ComparisonOperator IN is not valid for L AttributeValue typeScanFilter / QueryFilterFilterExpression with IN (:a, :b)
Action DELETE is not supported for the type NAttributeUpdatesUpdateExpression; DELETE works only on sets
Condition parameter type does not match schema typeKeyConditionsKeyConditionExpression with a correctly typed value

The pattern behind the ComparisonOperator messages is that each operator expects a specific number and type of values: CONTAINS takes exactly one value, a single string, number or binary, never a set or list, and IN takes scalar values, never a list. If you maintain code that uses these parameters, rewriting the call with an expression usually makes the error disappear and the code easier to read. Rewrite the whole call at once: a request that mixes the two styles fails with "Can not use both expression and non-expression parameters in the same request".

Size and number limits: 2,048 bytes, 1,024 bytes, 400 KB, 38 digits

Some of these messages are about size rather than shape. These are DynamoDB's hard limits, and none of them can be raised:

LimitValueWhat happens past it
Partition key value1 to 2,048 bytes"Size of hashkey has exceeded the maximum size limit"
Sort key value1 to 1,024 bytesThe write is refused with a key size message
Index key valuesSame as aboveWrites refused; existing items skipped by the backfill
Item size400 KB, attribute names included"Item size has exceeded the maximum allowed size"
Number precision38 digitsThe write is refused
Number range1E-130 to 9.99...E+125, positive or negativeThe write is refused
Nesting depth32 levels of maps and listsThe write is refused
Expression length4 KB per expression stringThe request is refused

Sizes are measured in UTF-8 bytes, not characters, so a key made of emoji or non-Latin text reaches the limit sooner than its character count suggests. A partition key over 2,048 bytes almost always means something that was never meant to be a key, such as a full URL with a long query string, a JSON document or a description, ended up there. Hash it instead (a SHA-256 hex digest is 64 bytes) and store the original as a normal attribute.

Numbers have their own trap in JavaScript. A JavaScript number can hold integers exactly only up to 9,007,199,254,740,991, so the DocumentClient refuses larger ones with "Use NumberValue from @aws-sdk/lib-dynamodb" rather than silently rounding them. Store IDs from other systems, such as order numbers or snowflake IDs, as strings. The 400 KB item limit has its own guide: our item size guide covers splitting items and moving large values to S3.

DynamoDB Local words these errors differently

If you develop against DynamoDB Local (the downloadable JAR or the amazon/dynamodb-local Docker image), or a tool built on it such as Amplify's local mock, you'll meet different messages for the same mistakes. That matters, because searching for Local's wording leads you to fewer answers, and some mistakes that Local accepts fail in AWS. Here are the differences that come up most, with DynamoDB Local 3.3.1:

What you didDynamoDB in AWSDynamoDB Local 3.3.1
PutItem without the keyOne or more parameter values were invalid: Missing the key id in the itemOne of the required keys was not given a value
Number for a string key... Type mismatch for key id expected: S actual: N... Type mismatch for key (no name or types)
A map or null as the key... Type mismatch for key id expected: S actual: M (or NULL)Invalid attribute value type
Wrong type for an index key... Type mismatch for Index Key GSI1PK Expected: S Actual: N IndexName: GSI1... Type mismatch for Index Key (no details)
Partition key over 2,048 bytes... Size of hashkey has exceeded the maximum size limitHash primary key values must be under 2048 bytes, and range primary key values must be under 1024 bytes
Non-key attribute in AttributeDefinitionsNumber of attributes in KeySchema does not exactly match ... / Some AttributeDefinitions are not usedThe number of attributes in key schema must match the number of attributes defined in attribute definitions.
Index key missing from AttributeDefinitionsSome index key attributes are not defined in AttributeDefinitionsAttribute email is not defined in AttributeDefinitions
On-demand table with ProvisionedThroughputNeither ReadCapacityUnits nor WriteCapacityUnits can be specified ...Accepted; the table is created
Adding an index over items with a wrongly typed attributeThose items are left out of the indexThose items still count in the index

So "One of the required keys was not given a value" is simply DynamoDB Local's way of saying "Missing the key". Treat the two as the same error. The bottom two rows are the dangerous ones: a template that works against Local can fail in AWS, and an index that looks complete locally can silently skip items in AWS. Run your table definitions and any new index against a real table before you rely on them.

See exactly what you sent: log the request in Python, Node.js and the CLI

Most of these errors come down to the gap between what you think you sent and what actually left your code. Closing that gap takes one line. Each SDK can print the exact JSON body of the request, with the type letters DynamoDB sees.

Python (boto3)

import logging
logging.basicConfig()
logging.getLogger("botocore.endpoint").setLevel(logging.DEBUG)

Every request then logs a line containing 'body': b'{"TableName": "Bookings", "Item": {...}}'. For the double-wrapping mistake from earlier it shows the problem at a glance: {"id": {"M": {"S": {"S": "B-1042"}}}}. Turn it off again when you're done; it logs full items, which may include personal data.

Node.js (AWS SDK v3)

const client = new DynamoDBClient({});
client.middlewareStack.add(
  (next) => async (args) => {
    const b = args.request.body;
    console.log("Sending:", typeof b === "string" ? b : new TextDecoder().decode(b));
    return next(args);
  },
  { step: "build", name: "logDynamoBody" }
);
const doc = DynamoDBDocumentClient.from(client);

For the Lambda trap, it prints Sending: {"TableName":"Bookings","Item":{}}, which proves the id never left the function. Add the middleware to the base client before wrapping it in the DocumentClient, so you see the request after the DocumentClient has converted your values.

AWS CLI

aws dynamodb put-item --table-name Bookings \
  --item '{"id": {"S": "B-1042"}, "name": {"S": "Jake"}}' --debug 2>&1 | grep "Making request"

--debug prints a "Making request for OperationModel(name=PutItem) with params" line that includes the body. In the CLI, --item always uses the typed format, so a value without its type letter is the most common CLI mistake.

Compare the logged body with the describe-table output from the first section: the key names must match exactly, and each key's type letter must match its AttributeType. If they match and an index exists, check the index key attributes the same way. Our boto3 error handling guide shows how to catch a ValidationException by its code and log the message without crashing the rest of a batch.

Still stuck? Troubleshooting by symptom

It works in the Lambda console test but fails from the website

The test event is the plain JSON you typed, but API Gateway's proxy event puts the request JSON in event.body as a string. Parse event.body (decoding base64 if isBase64Encoded is true) and read the key from the parsed object. Validate it before calling DynamoDB.

It works against DynamoDB Local but fails in AWS

Local accepts some mistakes, such as ProvisionedThroughput on an on-demand table, and doesn't model the index backfill. Compare your request with the differences table, and test table definitions against a real table in a development account.

Only some old items fail to update after adding an index

Those items hold a value for the new index key with the wrong type, too many bytes, or an empty string. The backfill skipped them, and DynamoDB now refuses ordinary updates to them. Find them with the scan script in the index section, then fix or remove that one attribute.

It fails only for some customers or IDs

Look at what's different about the failing values: a numeric ID sent as a number, an empty field, a very long value, or a list. Logging the request body for one failing call usually shows the difference immediately.

A CloudFormation update fails with the ReadCapacityUnits message

The table is on-demand but a ProvisionedThroughput block is still in the template, on the table or on an index. Remove every one. If the stack rolled back, it returns to its previous working state, and you can update again.

The message says "One of the required keys was not given a value"

You're talking to DynamoDB Local or a mock built on it, such as Amplify's local API mock. It means the same as "Missing the key ... in the item": the item lacks the table's partition or sort key. Fix it the same way.

DynamoDB "One or more parameter values were invalid": frequently asked questions

What does "One or more parameter values were invalid" mean in DynamoDB?

DynamoDB refused the request during validation, before reading or writing anything, and returned HTTP 400 ValidationException. The words after the colon name the exact problem, such as a missing key, a type mismatch, an empty set or a bad table definition. Retrying the same request will always fail; the request itself must change.

How do I fix "Missing the key id in the item"?

Make sure every item includes the table's partition key, and sort key if it has one, with exactly the same name and case. Run describe-table to see the key names. In Lambda behind API Gateway, parse event.body first, because event.id is undefined and the JavaScript SDK drops undefined attributes before sending.

What does "Type mismatch for key expected: S actual: M" mean?

The table key is a string, but you sent a map. Usually you passed a whole object as the key, or used boto3's Table resource or the DocumentClient with values already in typed form like {"S": "x"}, so they were wrapped twice. Send a plain string with the resource or DocumentClient, typed values with the low-level client.

What does "Type mismatch for Index Key" mean?

A global secondary index has a key attribute of one type, and your item has that attribute with another type, for example a number where the index expects a string. DynamoDB enforces index key types on every write to the table. Send the correct type, or leave the attribute out if the item shouldn't appear in the index.

What is "Schema violation against backfilling index" in DynamoDB?

It's the same index key problem reported while a new global secondary index is still being built. A write gave the index key attribute a type or value the new index can't accept. Fix the value's type, or omit the attribute. Existing items with bad values are skipped by the backfill and refuse later updates until fixed.

Can a DynamoDB partition key be an empty string?

No. Partition keys, sort keys and the key attributes of every secondary index must be at least one byte long. An empty key returns "The AttributeValue for a key attribute cannot contain an empty string value." Refuse the request, generate an ID, or leave optional index attributes out of the item.

Does DynamoDB allow empty strings?

Yes, for attributes that aren't keys, since May 18, 2020. Empty strings and binary values are also allowed inside lists, maps and sets. Key attributes of the table and its indexes still can't be empty. The old message "An AttributeValue may not contain an empty string" means an engine or library older than that change.

How do I fix "An string set may not be empty"?

DynamoDB sets must have at least one member. Leave the attribute out when the set is empty, use REMOVE instead of setting an empty set, and use DELETE to remove members, which drops the attribute when the last member goes. In Python, an empty set() is sent as a number set, so the message may say "number set".

What does "Condition parameter type does not match schema type" mean?

A Query's key condition compares a key with a value of the wrong type, such as a number for a string partition key. Query string and path parameters are always strings, so convert them to Decimal or int when the key is numeric. It also appears when querying an index with the wrong type for its key.

What does "Number of attributes in KeySchema does not exactly match number of attributes defined in AttributeDefinitions" mean?

Your CreateTable or template lists attributes in AttributeDefinitions that aren't keys. AttributeDefinitions must contain only the table's key attributes and the key attributes of its indexes. Remove every other attribute; items can still store them without being declared.

Do I need to define all attributes when creating a DynamoDB table?

No. Define only the attributes used as keys, for the table and for each secondary index. DynamoDB is schemaless for everything else, so each item can carry any other attributes. Declaring extra attributes is an error in CreateTable, CloudFormation and Terraform.

Why does DynamoDB Local say "One of the required keys was not given a value"?

It's DynamoDB Local's wording for a missing key; the real service returns "Missing the key id in the item" for the same mistake. Tools built on DynamoDB Local, such as Amplify's local mock, show it too. Add the table's partition key, and sort key if any, to the item.

What does "Null attribute value types must have the value of true" mean?

Something sent {"NULL": false}. In DynamoDB's typed format a null is always {"NULL": true}; false isn't valid. Store the value with its real type instead, or leave the attribute out of the item or REMOVE it if you mean "no value".

How do I fix "Neither ReadCapacityUnits nor WriteCapacityUnits can be specified when BillingMode is PAY_PER_REQUEST"?

Remove every ProvisionedThroughput block from the table definition, including those under each global secondary index, because on-demand tables have no provisioned capacity. It often appears on a CloudFormation update after switching a table to on-demand while leaving the old capacity settings in the template.

Can I change the partition key value of an existing DynamoDB item?

No. UpdateItem refuses to modify key attributes. Write a new item with the new key and delete the old one, ideally together in TransactWriteItems with a condition that the new key doesn't already exist. Index key attributes, by contrast, can be updated normally.

What is the maximum size of a DynamoDB partition key?

2,048 bytes, and a sort key can be up to 1,024 bytes, measured in UTF-8 bytes. Index keys have the same limits. A longer partition key fails with "Size of hashkey has exceeded the maximum size limit". Hash long values and store the original in a normal attribute.

Why does removeUndefinedValues still give "Missing the key"?

removeUndefinedValues removes undefined attributes from the request, and an undefined key is removed along with everything else, so DynamoDB still receives an item without its key. The JavaScript DocumentClient already drops undefined top-level attributes. Validate that the key has a value before calling PutCommand.

How do I see the request boto3 sends to DynamoDB?

Set the botocore.endpoint logger to DEBUG with logging.getLogger("botocore.endpoint").setLevel(logging.DEBUG) after logging.basicConfig(). Each request then logs its JSON body, including the type letters DynamoDB sees. Turn it off afterwards, because it logs full items.

πŸ“š ALSO READ

Where to go next, from the other DynamoDB ValidationExceptions to the Lambda and API Gateway side of the same request:

📌 Bookmark this; the decoder table covers every message that can follow the colon.

Jake's booking form now checks for a booking number before it calls DynamoDB, so a customer with a blank field gets "please fill in the booking number" instead of "something went wrong." The dozen old bookings with numeric statuses were found by Ethan's scan script in under a minute and fixed with one update each, and the "list by status" page now shows every booking. Ethan also added the one-line request log to the function's debug switch, so the next mystery takes thirty seconds to read instead of an afternoon to guess. He wrote the line below on the whiteboard behind the counter.

📌 If you keep one line from this page

DynamoDB tells you exactly what's wrong after the colon; log the request you really sent, and the two will always point at the same field.

Keys must be present, typed and non-empty; AttributeDefinitions lists keys only; an index checks its key types on every write.

Revision note. Written October 12, 2026, for every booking that vanished between the form and the table. "Measure twice, cut once," the carpenters say; with DynamoDB, read the colon twice and log once.

Related