Repository: jeremydaly/dynamodb-toolbox (ts)

Issue:
Way to skip Exception/Error when field does not have a mapping or alias
Hello there,

While using the `Entity.putBatch` followed by `table.batchWrite()` I incurred in the following error:
`Error: Field 'newField' does not have a mapping or alias`

The entity I want to persist comes from an external API, so it can happen that some new fields are added without notice, and that's exactly what happened in this case.
Is there a way to instruct dynamodb-toolbox to just "skip" persistence of unknown fields, instead
of rising this error?

Or is there a better way to approach this kind of problem?

@jeremydaly Thank you very much for the help!

Pull request description:
feat(entity): add strictSchemaCheck option
- allow to filter unmapped fields without throwing an error in put/update operations (including transactions) 

closes #230

Patch:
diff --git a/docs/docs/entity/methods.md b/docs/docs/entity/methods.md
index 03ee184b..65dca12b 100644
--- a/docs/docs/entity/methods.md
+++ b/docs/docs/entity/methods.md
@@ -97,14 +97,14 @@ The `put` method accepts three arguments. The first argument accepts an `object`
 The optional second argument accepts an `options` object. The following options are all optional (corresponding PutItem API references in parentheses):
 
 | Option       |        Type         | Description                                                                                                                                                                                                                                           |
-| ------------ | :-----------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| ------------ | :-----------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | conditions   | `array` or `object` | A complex `object` or `array` of objects that specifies the conditions that must be met to put the item. See [Filters and Conditions](/docs/filters-and-conditions). (ConditionExpression)                                                            |
 | capacity     |      `string`       | Return the amount of consumed capacity. One of either `none`, `total`, or `indexes` (ReturnConsumedCapacity)                                                                                                                                          |
 | metrics      |      `string`       | Return item collection metrics. If set to `size`, the response includes statistics about item collections, if any, that were modified during the operation are returned in the response. One of either `none` or `size` (ReturnItemCollectionMetrics) |
 | returnValues |      `string`       | Determines whether to return item attributes as they appeared before a new item was added. One of either `none` or `all_old`. (ReturnValues)                                                                                                          |
 | execute      |      `boolean`      | Enables/disables automatic execution of the DocumentClient method (default: _inherited from Entity_)                                                                                                                                                  |
 | parse        |      `boolean`      | Enables/disables automatic parsing of returned data when `autoExecute` evaluates to `true` (default: _inherited from Entity_)                                                                                                                         |
-
+| strictSchemaCheck |  `boolean`       | Determines whether to throw an error or filter returned attributes when unmapped fields are provided in the request                                                                                                                                                                |
 If you prefer to specify your own parameters, the optional third argument allows you to add custom parameters. [See Adding custom parameters and clauses](/docs/custom-parameters) for more information.
 
 ```typescript
@@ -133,13 +133,14 @@ The `update` method accepts three arguments. The first argument accepts an `obje
 The optional second argument accepts an `options` object. The following options are all optional (corresponding UpdateItem API references in parentheses):
 
 | Option       |        Type         | Description                                                                                                                                                                                                                                           |
-| ------------ | :-----------------: | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| ------------ | :-----------------: |-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
 | conditions   | `array` or `object` | A complex `object` or `array` of objects that specifies the conditions that must be met to update the item. See [Filters and Conditions](/docs/filters-and-conditions). (ConditionExpression)                                                         |
 | capacity     |      `string`       | Return the amount of consumed capacity. One of either `none`, `total`, or `indexes` (ReturnConsumedCapacity)                                                                                                                                          |
 | metrics      |      `string`       | Return item collection metrics. If set to `size`, the response includes statistics about item collections, if any, that were modified during the operation are returned in the response. One of either `none` or `size` (ReturnItemCollectionMetrics) |
 | returnValues |      `string`       | Determines whether to return item attributes as they appeared before or after the item was updated. One of either `none`, `all_old`, `updated_old`, `all_new`, `updated_new`. (ReturnValues)                                                          |
 | execute      |      `boolean`      | Enables/disables automatic execution of the DocumentClient method (default: _inherited from Entity_)                                                                                                                                                  |
 | parse        |      `boolean`      | Enables/disables automatic parsing of returned data when `autoExecute` evaluates to `true` (default: _inherited from Entity_)                                                                                                                         |
+| strictSchemaCheck |  `boolean`       | Determines whether to throw an error or filter returned attributes when unmapped fields are provided in the request                                                                                                                                                                |
 
 If you prefer to specify your own parameters, the optional third argument allows you to add custom parameters and clauses. [See Adding custom parameters and clauses](/docs/custom-parameters) for more information.
 
diff --git a/docs/docs/table/methods.md b/docs/docs/table/methods.md
index 77963e69..10ea6967 100644
--- a/docs/docs/table/methods.md
+++ b/docs/docs/table/methods.md
@@ -265,7 +265,8 @@ const result = await Default.transactWrite(
     Pet.conditionCheck({ family: 'Brady', name: 'Tiger' }, { conditions: { attr: 'alive', eq: false } },
     Pet.deleteTransaction({ family: 'Brady', name: 'Tiger' }),
     User.putTransaction({ family: 'Brady', name: 'Carol', age: 40, roles: ['mother','wife'] }),
-    User.putTransaction({ family: 'Brady', name: 'Mike', age: 42, roles: ['father','husband'] })
+    User.putTransaction({ family: 'Brady', name: 'Mike', age: 42, roles: ['father','husband'] }),
+    User.putTransaction({ family: 'Brady', name: 'Mike', age: 42, unmappedField: 'unmappedValue' }, { strictSchemaCheck: false})
   ],{
     capacity: 'total',
     metrics: 'size',
diff --git a/src/classes/Entity/Entity.ts b/src/classes/Entity/Entity.ts
index a7bbf610..e9039290 100644
--- a/src/classes/Entity/Entity.ts
+++ b/src/classes/Entity/Entity.ts
@@ -44,8 +44,8 @@ import type {
   UpdateItem,
   UpdateOptionsReturnValues,
   Writable,
-  Readonly
-} from './types'
+  Readonly, $PutBatchOptions,
+} from './types';
 
 class Entity<
   Name extends string = string,
@@ -797,10 +797,11 @@ class Entity<
     ResponseAttributes extends ShownItemAttributes = ShownItemAttributes,
     ReturnValues extends UpdateOptionsReturnValues = 'NONE',
     Execute extends boolean | undefined = undefined,
-    Parse extends boolean | undefined = undefined
+    Parse extends boolean | undefined = undefined,
+    StrictSchemaCheck extends boolean | undefined= true
   >(
-    item: UpdateItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes>,
-    options: $UpdateOptions<ResponseAttributes, ReturnValues, Execute, Parse> = {},
+    item: UpdateItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes, StrictSchemaCheck>,
+    options: $UpdateOptions<ResponseAttributes, ReturnValues, Execute, Parse, StrictSchemaCheck> = {},
     params: UpdateCustomParams = {}
   ): Promise<
     A.Compute<
@@ -831,7 +832,8 @@ class Entity<
       ResponseAttributes,
       ReturnValues,
       Execute,
-      Parse
+      Parse,
+      StrictSchemaCheck
     >(item, options, params)
 
     if (!shouldExecute(options.execute, this.autoExecute)) {
@@ -870,16 +872,18 @@ class Entity<
       Attributes['all'],
       keyof MethodItemOverlay
     >,
-    ResponseAttributes extends ItemAttributes = ItemAttributes
+    ResponseAttributes extends ItemAttributes = ItemAttributes,
+    StrictSchemaCheck extends boolean | undefined = true
   >(
-    item: UpdateItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes>,
-    options: TransactionOptions<ResponseAttributes> = {},
+    item: UpdateItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes, StrictSchemaCheck>,
+    options: TransactionOptions<ResponseAttributes, StrictSchemaCheck> = {},
     params?: UpdateCustomParams
   ): { Update: DocumentClient.Update } {
     // Destructure options to check for extraneous arguments
     const {
       conditions, // ConditionExpression
       returnValues, // ReturnValuesOnConditionCheckFailure (none, all_old)
+      strictSchemaCheck, // StrictSchemaCheck
       ...args
     } = options
 
@@ -893,7 +897,10 @@ class Entity<
       MethodItemOverlay,
       ItemAttributes,
       ResponseAttributes,
-      TransactionOptionsReturnValues
+      TransactionOptionsReturnValues,
+      undefined,
+      undefined,
+      StrictSchemaCheck
     >(item, options, params)
 
     // If ReturnValues exists, replace with ReturnValuesOnConditionCheckFailure
@@ -917,10 +924,11 @@ class Entity<
     ResponseAttributes extends ShownItemAttributes = ShownItemAttributes,
     ReturnValues extends UpdateOptionsReturnValues | TransactionOptionsReturnValues = 'NONE',
     Execute extends boolean | undefined = undefined,
-    Parse extends boolean | undefined = undefined
+    Parse extends boolean | undefined = undefined,
+    StrictSchemaCheck extends boolean | undefined = true
   >(
-    item: UpdateItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes>,
-    options: $UpdateOptions<ResponseAttributes, ReturnValues, Execute, Parse> = {},
+    item: UpdateItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes, StrictSchemaCheck>,
+    options: $UpdateOptions<ResponseAttributes, ReturnValues, Execute, Parse, StrictSchemaCheck> = {},
     {
       SET = [],
       REMOVE = [],
@@ -953,11 +961,14 @@ class Entity<
     // Initialize validateType with the DocumentClient
     const validateType = validateTypes(this.DocumentClient)
 
+     const shouldFilterUnmappedFields = options.strictSchemaCheck === false
+
     // Merge defaults
     const data = normalizeData(this.DocumentClient)(
       schema.attributes,
       linked,
-      Object.assign({}, defaults, item)
+      Object.assign({}, defaults, item),
+      shouldFilterUnmappedFields
     )
 
     // Extract valid options
@@ -966,6 +977,7 @@ class Entity<
       capacity, // ReturnConsumedCapacity (none, total, or indexes)
       metrics, // ReturnItemCollectionMetrics: (size or none)
       returnValues, // Return Values (none, all_old, updated_old, all_new, updated_new)
+      strictSchemaCheck, // Strict Schema Check (true or false)
       ..._args
     } = options
 
@@ -1297,10 +1309,11 @@ class Entity<
     ResponseAttributes extends ShownItemAttributes = ShownItemAttributes,
     ReturnValues extends PutOptionsReturnValues = 'NONE',
     Execute extends boolean | undefined = undefined,
-    Parse extends boolean | undefined = undefined
+    Parse extends boolean | undefined = undefined,
+    StrictSchemaCheck extends boolean | undefined= true
   >(
-    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes>,
-    options: $PutOptions<ResponseAttributes, ReturnValues, Execute, Parse> = {},
+    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes, StrictSchemaCheck>,
+    options: $PutOptions<ResponseAttributes, ReturnValues, Execute, Parse, StrictSchemaCheck> = {},
     params: Partial<DocumentClient.PutItemInput> = {}
   ): Promise<
     If<
@@ -1330,7 +1343,8 @@ class Entity<
       ResponseAttributes,
       ReturnValues,
       Execute,
-      Parse
+      Parse,
+      StrictSchemaCheck
     >(item, options, params)
 
     if (!shouldExecute(options.execute, this.autoExecute)) {
@@ -1357,14 +1371,27 @@ class Entity<
   /**
    * Generate parameters for PUT batch operation
    * @param {object} item - The item you wish to put.
+   * @param {object} options - Options for the operation.
    *
    * Only Item is supported (e.g. no conditions)
    *   https://docs.aws.amazon.com/amazondynamodb/latest/APIReference/API_BatchWriteItem.html
    */
-  putBatch<MethodItemOverlay extends Overlay = undefined>(
-    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes>
+  putBatch<
+    MethodItemOverlay extends Overlay = undefined,
+    ShownItemAttributes extends A.Key = If<
+      A.Equals<MethodItemOverlay, undefined>,
+      Attributes['shown'],
+      keyof MethodItemOverlay
+    >,
+    ResponseAttributes extends ShownItemAttributes = ShownItemAttributes,
+    Execute extends boolean | undefined = undefined,
+    Parse extends boolean | undefined = undefined,
+    StrictSchemaCheck extends boolean | undefined = true
+    >(
+    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes, StrictSchemaCheck>,
+    options: $PutBatchOptions<Execute, Parse, StrictSchemaCheck> = {}
   ): { [key: string]: DocumentClient.WriteRequest } {
-    const payload = this.putParams<MethodItemOverlay>(item)
+    const payload = this.putParams<MethodItemOverlay, ShownItemAttributes, ResponseAttributes, 'NONE', Execute, Parse, StrictSchemaCheck>(item, options)
     return { [payload.TableName]: { PutRequest: { Item: payload.Item } } }
   }
 
@@ -1383,16 +1410,18 @@ class Entity<
       Attributes['all'],
       keyof MethodItemOverlay
     >,
-    ResponseAttributes extends ItemAttributes = ItemAttributes
+    ResponseAttributes extends ItemAttributes = ItemAttributes,
+    StrictSchemaCheck extends boolean | undefined = true
   >(
-    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes>,
-    options: TransactionOptions<ResponseAttributes> = {},
+    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes, StrictSchemaCheck>,
+    options: TransactionOptions<ResponseAttributes, StrictSchemaCheck> = {},
     params?: Partial<DocumentClient.PutItemInput>
   ): { Put: DocumentClient.Put } {
     // Destructure options to check for extraneous arguments
     const {
       conditions, // ConditionExpression
       returnValues, // ReturnValuesOnConditionCheckFailure (none, all_old)
+      strictSchemaCheck, // StrictSchemaCheck
       ...args
     } = options
 
@@ -1406,9 +1435,13 @@ class Entity<
       MethodItemOverlay,
       ItemAttributes,
       ResponseAttributes,
-      TransactionOptionsReturnValues
+      TransactionOptionsReturnValues,
+      undefined,
+      undefined,
+      StrictSchemaCheck
     >(item, options, params)
 
+
     // If ReturnValues exists, replace with ReturnValuesOnConditionCheckFailure
     if ('ReturnValues' in payload) {
       let { ReturnValues, ..._payload } = payload
@@ -1430,10 +1463,11 @@ class Entity<
     ResponseAttributes extends ShownItemAttributes = ShownItemAttributes,
     ReturnValues extends PutOptionsReturnValues = 'NONE',
     Execute extends boolean | undefined = undefined,
-    Parse extends boolean | undefined = undefined
+    Parse extends boolean | undefined = undefined,
+    StrictSchemaCheck extends boolean | undefined = true
   >(
-    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes>,
-    options: $PutOptions<ResponseAttributes, ReturnValues, Execute, Parse> = {},
+    item: PutItem<MethodItemOverlay, EntityItemOverlay, CompositePrimaryKey, Item, Attributes, StrictSchemaCheck>,
+    options: $PutOptions<ResponseAttributes, ReturnValues, Execute, Parse, StrictSchemaCheck> = {},
     params: Partial<DocumentClient.PutItemInput> = {}
   ): DocumentClient.PutItemInput {
     // Extract schema and defaults
@@ -1442,11 +1476,14 @@ class Entity<
     // Initialize validateType with the DocumentClient
     const validateType = validateTypes(this.DocumentClient)
 
+    const shouldFilterUnmappedFields = options.strictSchemaCheck === false
+
     // Merge defaults
     const data = normalizeData(this.DocumentClient)(
       schema.attributes,
       linked,
-      Object.assign({}, defaults, item)
+      Object.assign({}, defaults, item),
+      shouldFilterUnmappedFields
     )
 
     // Extract valid options
@@ -1455,6 +1492,7 @@ class Entity<
       capacity, // ReturnConsumedCapacity (none, total, or indexes)
       metrics, // ReturnItemCollectionMetrics: (size or none)
       returnValues, // Return Values (none, all_old, updated_old, all_new, updated_new)
+      strictSchemaCheck, // Strict Schema Check (true or false)
       ..._args
     } = options
 
diff --git a/src/classes/Entity/types.ts b/src/classes/Entity/types.ts
index ed72437f..1b893053 100644
--- a/src/classes/Entity/types.ts
+++ b/src/classes/Entity/types.ts
@@ -378,15 +378,26 @@ export type $PutOptions<
   Attributes extends A.Key = A.Key,
   ReturnValues extends PutOptionsReturnValues = PutOptionsReturnValues,
   Execute extends boolean | undefined = undefined,
-  Parse extends boolean | undefined = undefined
-> = O.Partial<$WriteOptions<Attributes, Execute, Parse> & { returnValues: ReturnValues }>
+  Parse extends boolean | undefined = undefined,
+  StrictSchemaCheck extends boolean | undefined = true
+> = O.Partial<$WriteOptions<Attributes, Execute, Parse> & { returnValues: ReturnValues, strictSchemaCheck?: StrictSchemaCheck }>
+
+export type $PutBatchOptions<
+  Execute extends boolean | undefined = undefined,
+  Parse extends boolean | undefined = undefined,
+  StrictSchemaCheck extends boolean | undefined = true
+> = O.Partial<
+  Pick<BaseOptions<Execute, Parse>, 'execute' |'parse'>
+  & { strictSchemaCheck?: StrictSchemaCheck }
+>
 
 export type PutItem<
   MethodItemOverlay extends Overlay,
   EntityItemOverlay extends Overlay,
   CompositePrimaryKey extends O.Object,
   Item extends O.Object,
-  Attributes extends ParsedAttributes
+  Attributes extends ParsedAttributes,
+  StrictSchemaCheck extends boolean | undefined = true
 > = FirstDefined<
   [
     MethodItemOverlay,
@@ -399,7 +410,7 @@ export type PutItem<
             O.Update<Item, Attributes['optional'], A.x | null>
         >
     >
-  ]
+  ] | If<A.Equals<StrictSchemaCheck, true>, never, any>
 >
 
 export type UpdateOptionsReturnValues =
@@ -413,8 +424,9 @@ export type $UpdateOptions<
   Attributes extends A.Key = A.Key,
   ReturnValues extends UpdateOptionsReturnValues = UpdateOptionsReturnValues,
   Execute extends boolean | undefined = undefined,
-  Parse extends boolean | undefined = undefined
-> = O.Partial<$WriteOptions<Attributes, Execute, Parse> & { returnValues: ReturnValues }>
+  Parse extends boolean | undefined = undefined,
+  StrictSchemaCheck extends boolean | undefined = true
+> = O.Partial<$WriteOptions<Attributes, Execute, Parse> & { returnValues: ReturnValues, strictSchemaCheck?: StrictSchemaCheck }>
 
 export interface UpdateCustomParameters {
   SET: string[]
@@ -430,7 +442,8 @@ export type UpdateItem<
   EntityItemOverlay extends Overlay,
   CompositePrimaryKey extends O.Object,
   Item extends O.Object,
-  Attributes extends ParsedAttributes
+  Attributes extends ParsedAttributes,
+  StrictSchemaCheck extends boolean | undefined = true
 > = FirstDefined<
   [
     MethodItemOverlay,
@@ -455,6 +468,7 @@ export type UpdateItem<
         } & { $remove?: Attributes['optional'] | Attributes['optional'][] }
     >
   ]
+  | If<A.Equals<StrictSchemaCheck, true>, never, any>
 >
 
 export type DeleteOptionsReturnValues = 'NONE' | 'ALL_OLD'
@@ -463,14 +477,18 @@ export type RawDeleteOptions<
   Attributes extends A.Key = A.Key,
   ReturnValues extends DeleteOptionsReturnValues = DeleteOptionsReturnValues,
   Execute extends boolean | undefined = undefined,
-  Parse extends boolean | undefined = undefined
+  Parse extends boolean | undefined = undefined,
 > = O.Partial<$WriteOptions<Attributes, Execute, Parse> & { returnValues: ReturnValues }>
 
 export type TransactionOptionsReturnValues = 'NONE' | 'ALL_OLD'
 
-export interface TransactionOptions<Attributes extends A.Key = A.Key> {
+export interface TransactionOptions<
+  Attributes extends A.Key = A.Key,
+  StrictSchemaCheck extends boolean | undefined = true
+  > {
   conditions?: ConditionsOrFilters<Attributes>
-  returnValues?: TransactionOptionsReturnValues
+  returnValues?: TransactionOptionsReturnValues,
+  strictSchemaCheck?: StrictSchemaCheck
 }
 
 export type ShouldExecute<Execute extends boolean | undefined, AutoExecute extends boolean> = B.Or<
@@ -543,7 +561,7 @@ export type QueryOptions<
 export type PutOptions<
   E extends Entity,
   A extends ParsedAttributes = ExtractAttributes<E>
-> = $PutOptions<A['all'], PutOptionsReturnValues, boolean | undefined, boolean | undefined>
+> = $PutOptions<A['all'], PutOptionsReturnValues, boolean | undefined, boolean | undefined, boolean | undefined>
 
 export type DeleteOptions<
   E extends Entity,
@@ -553,4 +571,4 @@ export type DeleteOptions<
 export type UpdateOptions<
   E extends Entity,
   A extends ParsedAttributes = ExtractAttributes<E>
-> = $UpdateOptions<A['all'], UpdateOptionsReturnValues, boolean | undefined, boolean | undefined>
+> = $UpdateOptions<A['all'], UpdateOptionsReturnValues, boolean | undefined, boolean | undefined, boolean | undefined>


Test patch:
diff --git a/src/__tests__/entity.put.unit.test.ts b/src/__tests__/entity.put.unit.test.ts
index 465fb6a5..6d7ae5c0 100644
--- a/src/__tests__/entity.put.unit.test.ts
+++ b/src/__tests__/entity.put.unit.test.ts
@@ -39,7 +39,7 @@ const TestEntity = new Entity({
     test_composite2: ['sort', 1, { save: false }]
   },
   table: TestTable
-} as const)
+})
 
 const TestTable2 = new Table({
   name: 'test-table',
@@ -230,17 +230,51 @@ describe('put', () => {
     expect(() => TestEntity.putParams()).toThrow(`'pk' or 'email' is required`)
   })
 
-  it('fails when using an undefined schema field', () => {
+  it('fails when using an undefined schema field and strictSchemaCheck is not provided', () => {
+    expect(() =>
+      TestEntity.putParams({
+        email: 'test-pk',
+        sort: 'test-sk',
+        // @ts-expect-error
+        unknown: '?'
+      })
+    ).toThrow(`Field 'unknown' does not have a mapping or alias`)
+  })
+
+  it('fails when using an undefined schema field and strictSchemaCheck is true', () => {
     expect(() =>
       TestEntity.putParams({
         email: 'test-pk',
         sort: 'test-sk',
         // @ts-expect-error
         unknown: '?'
+      }, {
+        strictSchemaCheck: true
       })
     ).toThrow(`Field 'unknown' does not have a mapping or alias`)
   })
 
+  it('creates an item when using an undefined schema field and strictSchemaCheck is false', () => {
+    expect(() =>
+      TestEntity.putParams({
+        email: 'test-pk',
+        sort: 'test-sk',
+        unknown: '?'
+      }, {
+        strictSchemaCheck: false
+      })
+    ).not.toThrow()
+  })
+
+  it('omits unmapped attributes when strictSchemaCheck is false.', () => {
+    let { Item } = TestEntity.putParams(
+      { email: 'x', sort: 'y', unknown: '?' },
+      { strictSchemaCheck: false }
+    )
+
+    expect(Item.unknown).toBeUndefined()
+  });
+
   it('fails when invalid string provided with no coercion', () => {
     expect(() =>
       TestEntity.putParams({
diff --git a/src/__tests__/entity.putBatch.unit.ts b/src/__tests__/entity.putBatch.unit.ts
new file mode 100644
index 00000000..3618cc72
--- /dev/null
+++ b/src/__tests__/entity.putBatch.unit.ts
@@ -0,0 +1,78 @@
+import { Table, Entity } from '../index'
+import { DocumentClient } from './bootstrap-tests'
+
+const TestTable = new Table({
+  name: 'test-table',
+  partitionKey: 'pk',
+  sortKey: 'sk',
+  DocumentClient,
+})
+
+const TestEntity = new Entity({
+  name: 'TestEntity',
+  autoExecute: false,
+  attributes: {
+    email: { type: 'string', partitionKey: true },
+    sort: { type: 'string', sortKey: true },
+    test_string: { type: 'string' },
+  },
+  table: TestTable,
+})
+
+describe('putBatch', () => {
+  it('fails when using an undefined schema field and strictSchemaCheck is not provided', () => {
+    expect(() =>
+      TestEntity.putBatch({
+        email: 'test-pk',
+        sort: 'test-sk',
+        // @ts-expect-error
+        unknown: '?',
+      }),
+    ).toThrow(`Field 'unknown' does not have a mapping or alias`)
+  })
+
+  it('fails when using an undefined schema field and strictSchemaCheck is true', () => {
+    expect(() =>
+      TestEntity.putBatch({
+        email: 'test-pk',
+        sort: 'test-sk',
+        // @ts-expect-error
+        unknown: '?',
+      }, {
+        strictSchemaCheck: true,
+      }),
+    ).toThrow(`Field 'unknown' does not have a mapping or alias`)
+  })
+
+  it('creates an item when using an undefined schema field and strictSchemaCheck is false', () => {
+    expect(() =>
+      TestEntity.putBatch({
+        email: 'test-pk',
+        sort: 'test-sk',
+        unknown: '?',
+      }, {
+        strictSchemaCheck: false,
+      }),
+    ).not.toThrow()
+  })
+
+  it('returns the result in the correct format', async () => {
+    const result = TestEntity.putBatch({
+      email: 'test-pk',
+      sort: 'test-sk',
+      test_string: 'test string',
+    })
+
+    expect(result).toEqual({
+      [TestTable.name]: {
+        PutRequest: {
+          Item: expect.objectContaining({
+            pk: 'test-pk',
+            sk: 'test-sk',
+            test_string: 'test string',
+          })
+        }
+      }
+    })
+  })
+})
diff --git a/src/__tests__/entity.transaction.unit.test.ts b/src/__tests__/entity.transaction.unit.test.ts
index a088212c..abd59a38 100644
--- a/src/__tests__/entity.transaction.unit.test.ts
+++ b/src/__tests__/entity.transaction.unit.test.ts
@@ -1,5 +1,5 @@
-import { Table, Entity } from '../index';
-import { DocumentClient } from './bootstrap-tests';
+import { Table, Entity } from '../index'
+import { DocumentClient } from './bootstrap-tests'
 
 const TestTable = new Table({
   name: 'test-table',
@@ -9,7 +9,7 @@ const TestTable = new Table({
   indexes: {
     GSI1: { partitionKey: 'GSI1pk' },
   },
-});
+})
 
 const TestEntity = new Entity({
   name: 'TestEntity',
@@ -20,24 +20,24 @@ const TestEntity = new Entity({
     testString: 'string',
   },
   table: TestTable,
-} as const);
+})
 
-const deleteParams = jest.spyOn(Entity.prototype, 'deleteParams').mockReturnThis();
-const putParams = jest.spyOn(Entity.prototype, 'putParams').mockReturnThis();
-const updateParams = jest.spyOn(Entity.prototype, 'updateParams').mockReturnThis();
+const deleteParams = jest.spyOn(Entity.prototype, 'deleteParams')
+const putParams = jest.spyOn(Entity.prototype, 'putParams')
+const updateParams = jest.spyOn(Entity.prototype, 'updateParams')
 
 describe('Entity transactional operations', () => {
   afterEach(() => {
-    jest.clearAllMocks();
-  });
+    jest.clearAllMocks()
+  })
 
-    describe('deleteTransaction', () => {
+  describe('deleteTransaction', () => {
     it('throws an error when given options that are not conditions or returnValues.', async () => {
       expect(() => {
         // @ts-expect-error
-        TestEntity.deleteTransaction({ pk: 'some-pk', sk: 'some-sk' }, { invalidOption: true });
-      }).toThrow(`Invalid delete transaction options: invalidOption`);
-    });
+        TestEntity.deleteTransaction({ pk: 'some-pk', sk: 'some-sk' }, { invalidOption: true })
+      }).toThrow(`Invalid delete transaction options: invalidOption`)
+    })
 
     it('allows to provide conditions or returnValues as options.', async () => {
       expect(() => {
@@ -47,9 +47,9 @@ describe('Entity transactional operations', () => {
             attr: 'testString',
             exists: true,
           },
-        });
-      }).not.toThrow();
-    });
+        })
+      }).not.toThrow()
+    })
 
     it('passes the correct parameters to deleteParams.', async () => {
       TestEntity.deleteTransaction(
@@ -62,7 +62,7 @@ describe('Entity transactional operations', () => {
             ':pk': 'better-pk-to-delete',
           },
         },
-      );
+      )
 
       expect(deleteParams).toHaveBeenCalledWith(
         {
@@ -75,8 +75,8 @@ describe('Entity transactional operations', () => {
             ':pk': 'better-pk-to-delete',
           },
         },
-      );
-    });
+      )
+    })
 
     it('transforms ReturnValues into ReturnValuesOnConditionCheckFailure if provided.', async () => {
       deleteParams.mockReturnValueOnce(
@@ -88,23 +88,23 @@ describe('Entity transactional operations', () => {
           },
           ReturnValues: 'ALL_OLD',
         },
-      );
+      )
 
       const result = TestEntity.deleteTransaction(
         { pk: 'some-pk', sk: 'some-sk' },
         {
           returnValues: 'ALL_OLD',
         },
-      );
+      )
 
       // @ts-expect-error
-      expect(result.ReturnValues).toBeUndefined();
+      expect(result.ReturnValues).toBeUndefined()
       expect(result).toEqual({
         Delete: expect.objectContaining({
           ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
         }),
-      });
-    });
+      })
+    })
 
     it('returns the item in transaction format.', async () => {
       deleteParams.mockReturnValueOnce({
@@ -113,10 +113,10 @@ describe('Entity transactional operations', () => {
           pk: 'some-pk',
           sk: 'some-sk',
         }),
-      });
+      })
 
       const result = TestEntity.deleteTransaction(
-        { pk: 'some-pk', sk: 'some-sk' });
+        { pk: 'some-pk', sk: 'some-sk' })
 
       expect(result).toEqual({
         Delete: {
@@ -126,18 +126,18 @@ describe('Entity transactional operations', () => {
             sk: 'some-sk',
           },
         },
-      });
-    });
-  });
+      })
+    })
+  })
 
 
   describe('putTransaction', () => {
     it('throws an error when given options that are not conditions or returnValues.', async () => {
       expect(() => {
         // @ts-expect-error
-        TestEntity.putTransaction({ pk: 'some-pk', sk: 'some-sk' }, { invalidOption: true });
-      }).toThrow(`Invalid put transaction options: invalidOption`);
-    });
+        TestEntity.putTransaction({ pk: 'some-pk', sk: 'some-sk' }, { invalidOption: true })
+      }).toThrow(`Invalid put transaction options: invalidOption`)
+    })
 
     it('allows to provide conditions or returnValues as options.', async () => {
       expect(() => {
@@ -147,9 +147,39 @@ describe('Entity transactional operations', () => {
             attr: 'testString',
             exists: true,
           },
-        });
-      }).not.toThrow();
-    });
+        })
+      }).not.toThrow()
+    })
+
+    it('allows adding non mapped fields when strictSchemaCheck is false.', () => {
+      expect(() => TestEntity.putTransaction({
+        pk: 'test',
+        sk: 'testsk',
+        unknown: '?',
+      }, {
+        strictSchemaCheck: false,
+      })).not.toThrow()
+    })
+
+    it('omits unmapped attributes when strictSchemaCheck is false.', () => {
+      let { Put: { Item } } = TestEntity.putTransaction(
+        { pk: 'x', sk: 'y', unknown: '?' },
+        { strictSchemaCheck: false }
+      )
+
+      expect(Item.unknown).toBeUndefined()
+    })
+
+    it('throws an error when adding non mapped fields when strictSchemaCheck is true.', () => {
+      expect(() => TestEntity.putTransaction({
+        pk: 'test',
+        sk: 'testsk',
+        // @ts-expect-error
+        unknown: 'unknown',
+      }, {
+        strictSchemaCheck: true,
+      })).toThrow(`Field 'unknown' does not have a mapping or alias`)
+    })
 
     it('passes the correct parameters to putParams.', async () => {
       TestEntity.putTransaction(
@@ -162,7 +192,7 @@ describe('Entity transactional operations', () => {
             ':testString': 'best-test-string',
           },
         },
-      );
+      )
 
       expect(putParams).toHaveBeenCalledWith(
         {
@@ -176,8 +206,8 @@ describe('Entity transactional operations', () => {
             ':testString': 'best-test-string',
           },
         },
-      );
-    });
+      )
+    })
 
     it('transforms ReturnValues into ReturnValuesOnConditionCheckFailure if provided.', async () => {
       putParams.mockReturnValueOnce(
@@ -190,23 +220,23 @@ describe('Entity transactional operations', () => {
           },
           ReturnValues: 'ALL_OLD',
         },
-      );
+      )
 
       const result = TestEntity.putTransaction(
         { pk: 'some-pk', sk: 'some-sk', testString: 'some-test-string-with-change' },
         {
           returnValues: 'ALL_OLD',
         },
-      );
+      )
 
       // @ts-expect-error
-      expect(result.ReturnValues).toBeUndefined();
+      expect(result.ReturnValues).toBeUndefined()
       expect(result).toEqual({
         Put: expect.objectContaining({
           ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
         }),
-      });
-    });
+      })
+    })
 
     it('returns the item in transaction format.', async () => {
       putParams.mockReturnValueOnce({
@@ -216,10 +246,10 @@ describe('Entity transactional operations', () => {
           sk: 'some-sk',
           testString: 'some-test-string-with-change',
         }),
-      });
+      })
 
       const result = TestEntity.putTransaction(
-        { pk: 'some-pk', sk: 'some-sk', testString: 'some-test-string-with-change' });
+        { pk: 'some-pk', sk: 'some-sk', testString: 'some-test-string-with-change' })
 
       expect(result).toEqual({
         Put: {
@@ -230,17 +260,17 @@ describe('Entity transactional operations', () => {
             testString: 'some-test-string-with-change',
           },
         },
-      });
-    });
-  });
+      })
+    })
+  })
 
   describe('updateTransaction', () => {
     it('throws an error when given options that are not conditions or returnValues.', async () => {
       expect(() => {
         // @ts-expect-error
-        TestEntity.updateTransaction({ pk: 'some-pk', sk: 'some-sk' }, { invalidOption: true });
-      }).toThrow(`Invalid update transaction options: invalidOption`);
-    });
+        TestEntity.updateTransaction({ pk: 'some-pk', sk: 'some-sk' }, { invalidOption: true })
+      }).toThrow(`Invalid update transaction options: invalidOption`)
+    })
 
     it('allows to provide conditions or returnValues as options.', async () => {
       expect(() => {
@@ -250,9 +280,45 @@ describe('Entity transactional operations', () => {
             attr: 'testString',
             exists: true,
           },
-        });
-      }).not.toThrow();
-    });
+        })
+      }).not.toThrow()
+    })
+
+    it('allows adding non mapped fields when strictSchemaCheck is false.', () => {
+      expect(() => TestEntity.updateTransaction({
+        pk: 'test',
+        sk: 'testsk',
+        unknown: '?',
+      }, {
+        strictSchemaCheck: false,
+      })).not.toThrow()
+    })
+
+    it('omits unmapped attributes when strictSchemaCheck is false.', () => {
+      let { Update:
+        { UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues }
+      } = TestEntity.updateTransaction(
+        { pk: 'x', sk: 'y', testString: 'some-updated-test-string', unknown: '?' },
+        { strictSchemaCheck: false }
+      )
+
+      expect(UpdateExpression).not.toContain('#unknown')
+      expect(ExpressionAttributeNames).not.toHaveProperty('#unknown')
+      expect(ExpressionAttributeValues).not.toHaveProperty(':unknown')
+      expect(ExpressionAttributeNames).toHaveProperty('#testString')
+      expect(ExpressionAttributeValues).toHaveProperty(':testString')
+    })
+
+    it('throws an error when adding non mapped fields when strictSchemaCheck is true.', () => {
+      expect(() => TestEntity.updateTransaction({
+        pk: 'test',
+        sk: 'testsk',
+        // @ts-expect-error
+        unknown: 'unknown',
+      }, {
+        strictSchemaCheck: true,
+      })).toThrow(`Field 'unknown' does not have a mapping or alias`)
+    })
 
     it('passes the correct parameters to updateParams.', async () => {
       TestEntity.updateTransaction(
@@ -265,7 +331,7 @@ describe('Entity transactional operations', () => {
             ':testString': 'best-test-string',
           },
         },
-      );
+      )
 
       expect(updateParams).toHaveBeenCalledWith(
         {
@@ -279,8 +345,8 @@ describe('Entity transactional operations', () => {
             ':testString': 'best-test-string',
           },
         },
-      );
-    });
+      )
+    })
 
     it('transforms ReturnValues into ReturnValuesOnConditionCheckFailure if provided.', async () => {
       updateParams.mockReturnValueOnce(
@@ -293,23 +359,23 @@ describe('Entity transactional operations', () => {
           UpdateExpression: 'some-update-expression',
           ReturnValues: 'ALL_OLD',
         },
-      );
+      )
 
       const result = TestEntity.updateTransaction(
         { pk: 'some-pk', sk: 'some-sk', testString: 'some-test-string-with-change' },
         {
           returnValues: 'ALL_OLD',
         },
-      );
+      )
 
       // @ts-expect-error
-      expect(result.ReturnValues).toBeUndefined();
+      expect(result.ReturnValues).toBeUndefined()
       expect(result).toEqual({
         Update: expect.objectContaining({
           ReturnValuesOnConditionCheckFailure: 'ALL_OLD',
         }),
-      });
-    });
+      })
+    })
 
     it('returns the item in transaction format.', async () => {
       updateParams.mockReturnValueOnce({
@@ -319,10 +385,10 @@ describe('Entity transactional operations', () => {
           sk: 'some-sk',
         },
         UpdateExpression: 'some-update-expression',
-      });
+      })
 
       const result = TestEntity.updateTransaction(
-        { pk: 'some-pk', sk: 'some-sk', testString: 'some-test-string-with-change' });
+        { pk: 'some-pk', sk: 'some-sk', testString: 'some-test-string-with-change' })
 
       expect(result).toEqual({
         Update: {
@@ -333,8 +399,8 @@ describe('Entity transactional operations', () => {
           },
           UpdateExpression: 'some-update-expression'
         },
-      });
-    });
-  });
+      })
+    })
+  })
 
-});
+})
diff --git a/src/__tests__/entity.update.unit.test.ts b/src/__tests__/entity.update.unit.test.ts
index 542fc79e..88a14c25 100644
--- a/src/__tests__/entity.update.unit.test.ts
+++ b/src/__tests__/entity.update.unit.test.ts
@@ -1008,4 +1008,34 @@ describe('update', () => {
     )
     expect(TableName).toBe('test-table')
   })
+
+   it('fails when given an unmapped attribute and strictSchemaCheck is true.', () => {
+    expect(() =>
+      TestEntity.updateParams(
+        // @ts-expect-error
+        { email: 'x', sort: 'y', unknown: '?' },
+        { strictSchemaCheck: true }
+      )
+    ).toThrow("Field 'unknown' does not have a mapping or alias")
+  })
+
+  it('allows unmapped attributes when strictSchemaCheck is false.', () => {
+    expect(() => TestEntity.updateParams(
+      { email: 'x', sort: 'y', unknown: '?' },
+      { strictSchemaCheck: false }
+    )).not.toThrow();
+  })
+
+  it('omits unmapped attributes when strictSchemaCheck is false.', () => {
+    let { UpdateExpression, ExpressionAttributeNames, ExpressionAttributeValues } = TestEntity.updateParams(
+      { email: 'x', sort: 'y', test_string: 'some-string-value', unknown: '?' },
+      { strictSchemaCheck: false }
+    )
+
+    expect(UpdateExpression).not.toContain('#unknown')
+    expect(ExpressionAttributeNames).not.toHaveProperty('#unknown')
+    expect(ExpressionAttributeValues).not.toHaveProperty(':unknown')
+    expect(ExpressionAttributeNames).toHaveProperty('#test_string')
+    expect(ExpressionAttributeValues).toHaveProperty(':test_string')
+  });
 }) // end describe
diff --git a/src/__tests__/type-infering.unit.test.ts b/src/__tests__/type-infering.unit.test.ts
index 4289a5ab..d6b95a8b 100644
--- a/src/__tests__/type-infering.unit.test.ts
+++ b/src/__tests__/type-infering.unit.test.ts
@@ -72,7 +72,8 @@ type ExpectedWriteOpts<
   conditions: ConditionsOrFilters<Attributes>
   metrics: string
   include: string[]
-  returnValues: ReturnValues
+  returnValues: ReturnValues,
+  strictSchemaCheck: boolean
 }>
 
 describe('Entity', () => {
@@ -399,7 +400,7 @@ describe('Entity', () => {
         type DeleteItemOptions = DeleteOptions<typeof ent>
         type TestDeleteItemOptions = A.Equals<
           DeleteItemOptions,
-          ExpectedWriteOpts<keyof ExpectedItem | 'hidden', 'NONE' | 'ALL_OLD'>
+          Omit<ExpectedWriteOpts<keyof ExpectedItem | 'hidden', 'NONE' | 'ALL_OLD'>, 'strictSchemaCheck'>
         >
         const testDeleteItemOptions: TestDeleteItemOptions = 1
         testDeleteItemOptions
@@ -1140,7 +1141,7 @@ describe('Entity', () => {
         type DeleteItemOptions = DeleteOptions<typeof ent>
         type TestDeleteItemOptions = A.Equals<
           DeleteItemOptions,
-          ExpectedWriteOpts<keyof ExpectedItem | 'hidden', 'NONE' | 'ALL_OLD'>
+          Omit<ExpectedWriteOpts<keyof ExpectedItem | 'hidden', 'NONE' | 'ALL_OLD'>, 'strictSchemaCheck'>
         >
         const testDeleteItemOptions: TestDeleteItemOptions = 1
         testDeleteItemOptions
@@ -2298,7 +2299,7 @@ describe('Entity', () => {
           type DeleteItemOptions = DeleteOptions<typeof ent>
           type TestDeleteItemOptions = A.Equals<
             DeleteItemOptions,
-            ExpectedWriteOpts<keyof EntityItemOverlay, 'NONE' | 'ALL_OLD'>
+            Omit<ExpectedWriteOpts<keyof EntityItemOverlay, 'NONE' | 'ALL_OLD'>, 'strictSchemaCheck'>
           >
           const testDeleteItemOptions: TestDeleteItemOptions = 1
           testDeleteItemOptions