# How the Redux isPlainObject Utility Validates Actions: A Deep Dive into Source Code

> Understand how the Redux isPlainObject utility validates actions by examining its source code. Learn how it ensures actions are plain JS objects and rejects non-serializable types.

- Repository: [Redux/redux](https://github.com/reduxjs/redux)
- Tags: deep-dive
- Published: 2026-03-05

---

**The `isPlainObject` utility ensures Redux actions are plain JavaScript objects by traversing the prototype chain to verify inheritance from `Object.prototype` or `null`, rejecting class instances, arrays, dates, and other non-serializable types.**

The `isPlainObject` utility is a critical validation mechanism in the **reduxjs/redux** repository that enforces one of Redux's fundamental architectural constraints: every action must be a plain object. Located in [`src/utils/isPlainObject.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isPlainObject.ts), this lightweight function serves as the gatekeeper that prevents class instances, arrays, and other object subtypes from entering the dispatch pipeline, ensuring predictable state serialization and time-travel debugging capabilities.

## What Defines a Plain Object in Redux

Redux strictly requires that actions be **plain objects**—specifically, objects created via object literal syntax (`{}`) or `Object.create(null)`. This excludes class instances, arrays, `Date` objects, `Map` and `Set` instances, and any other object with a modified prototype chain.

The `isPlainObject` function implements this check by walking the prototype chain until it reaches the terminal `null` prototype, then comparing the original object's immediate prototype against this terminal value.

## How isPlainObject Works Under the Hood

The implementation in [`src/utils/isPlainObject.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isPlainObject.ts) uses a prototype traversal algorithm to determine object "plainness":

```typescript
// src/utils/isPlainObject.ts
export default function isPlainObject(obj: any): obj is object {
  if (typeof obj !== 'object' || obj === null) return false

  let proto = obj
  while (Object.getPrototypeOf(proto) !== null) {
    proto = Object.getPrototypeOf(proto)
  }

  return (
    Object.getPrototypeOf(obj) === proto ||
    Object.getPrototypeOf(obj) === null
  )
}

```

The function executes three distinct validation phases:

1. **Type Guard**: Immediately returns `false` for non-object types and `null` values
2. **Prototype Traversal**: Walks up the prototype chain using `Object.getPrototypeOf()` until reaching the terminal `null` prototype
3. **Equality Check**: Verifies the original object's prototype matches the terminal prototype (or is itself `null`)

This algorithm correctly identifies objects created via `Object.create(null)` as plain objects while rejecting instances of custom classes, which maintain their constructor prototypes in the chain.

## Action Validation with isAction

Redux builds upon `isPlainObject` with the `isAction` utility in [`src/utils/isAction.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isAction.ts) to enforce the complete action contract:

```typescript
// src/utils/isAction.ts
import type { Action } from '../types/actions'
import isPlainObject from './isPlainObject'

export default function isAction(action: unknown): action is Action<string> {
  return (
    isPlainObject(action) &&
    'type' in action &&
    typeof (action as Record<'type', unknown>).type === 'string'
  )
}

```

The `isAction` function performs sequential validation:

- Calls `isPlainObject(action)` to ensure the value is a plain object
- Checks for the presence of a `type` property using the `in` operator
- Validates that `action.type` is specifically a string type

This dual-layer validation ensures that only properly structured action objects reach the reducer pipeline.

## Where Redux Applies isPlainObject Checks

### Dispatch Validation in createStore

The primary enforcement point occurs in the `dispatch` function within [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts). Before any action reaches the reducers, Redux validates it using `isPlainObject`:

```typescript
// src/createStore.ts (excerpt)
function dispatch(action: A) {
  if (!isPlainObject(action)) {
    throw new Error(
      `Actions must be plain objects. Instead, the actual type was: '${kindOf(
        action
      )}'. You may need to add middleware to your store setup to handle dispatching other values such as functions or Promises.`
    )
  }
  // Additional validation for action.type follows...
}

```

This check throws a descriptive error immediately when developers attempt to dispatch class instances, arrays, or functions without appropriate middleware.

### State Validation in combineReducers

The `combineReducers` utility in [`src/combineReducers.ts`](https://github.com/reduxjs/redux/blob/main/src/combineReducers.ts) also employs `isPlainObject` to validate that reducer return values are plain objects, preventing accidental mutation of state shapes with non-plain object types.

## Practical Examples

### Identifying Plain Objects vs. Other Types

```typescript
import isPlainObject from 'redux/src/utils/isPlainObject'

// True cases - plain objects
console.log(isPlainObject({}))                    // true
console.log(isPlainObject({ type: 'ADD' }))       // true
console.log(isPlainObject(Object.create(null)))   // true

// False cases - not plain objects
console.log(isPlainObject([]))                    // false (array)
console.log(isPlainObject(new Date()))            // false (Date instance)
console.log(isPlainObject(new Map()))             // false (Map instance)
console.log(isPlainObject(function() {}))         // false (function)
console.log(isPlainObject(null))                  // false (null)
console.log(isPlainObject('string'))              // false (primitive)

// Class instances fail the check
class MyClass {}
console.log(isPlainObject(new MyClass()))          // false

```

### Manual Action Validation

```typescript
import isAction from 'redux/src/utils/isAction'

const validAction = { type: 'INCREMENT', payload: 1 }
const invalidAction1 = { payload: 1 }              // missing type
const invalidAction2 = { type: 123 }               // type not string
const invalidAction3 = new Map([['type', 'ADD']]) // not plain object

console.log(isAction(validAction))    // true
console.log(isAction(invalidAction1)) // false
console.log(isAction(invalidAction2)) // false
console.log(isAction(invalidAction3)) // false

```

### Catching Dispatch Errors

```typescript
import { createStore } from 'redux'

const reducer = (state = 0, action) => {
  if (action.type === 'INC') return state + 1
  return state
}

const store = createStore(reducer)

// This throws an error because functions are not plain objects
try {
  store.dispatch(() => ({ type: 'INC' }))
} catch (e) {
  console.error(e.message)
  // Output: Actions must be plain objects. Instead, the actual type was: 'function'. 
  // You may need to add middleware to your store setup...
}

```

## Summary

- **The `isPlainObject` utility** in [`src/utils/isPlainObject.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isPlainObject.ts) validates that a value is a plain JavaScript object by traversing its prototype chain to verify inheritance from `Object.prototype` or `null`.

- **Prototype chain traversal** distinguishes plain objects from class instances, arrays, dates, and other built-in objects, ensuring Redux actions remain serializable and predictable.

- **The `isAction` helper** in [`src/utils/isAction.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isAction.ts) combines `isPlainObject` with `type` property validation to enforce the complete Redux action contract.

- **Validation occurs at dispatch time** in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts), throwing descriptive errors when non-plain objects reach the store without appropriate middleware.

- **State shape protection** in [`src/combineReducers.ts`](https://github.com/reduxjs/redux/blob/main/src/combineReducers.ts) uses the same utility to ensure reducer return values maintain plain object structure.

## Frequently Asked Questions

### What is the difference between isPlainObject and isAction in Redux?

**`isPlainObject`** is a low-level utility that checks whether a value is a plain JavaScript object by analyzing its prototype chain, returning `true` only for objects created via `{}` or `Object.create(null)`. **`isAction`** builds upon this foundation in [`src/utils/isAction.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isAction.ts) by first calling `isPlainObject` and then verifying that the object contains a `type` property with a string value, ensuring compliance with the Redux action contract.

### Why does Redux reject class instances as actions?

Redux rejects class instances because they fail the `isPlainObject` check implemented in [`src/utils/isPlainObject.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isPlainObject.ts). Class instances maintain a prototype chain that includes the class constructor, not just `Object.prototype`, causing the prototype traversal algorithm to detect a mismatch. This restriction ensures actions remain **serializable** and **replayable**, which is essential for Redux DevTools time-travel debugging and state persistence.

### How does isPlainObject handle Object.create(null)?

The `isPlainObject` utility correctly identifies objects created via `Object.create(null)` as plain objects because it accepts prototypes of either `Object.prototype` or `null`. In [`src/utils/isPlainObject.ts`](https://github.com/reduxjs/redux/blob/main/src/utils/isPlainObject.ts), the function walks the prototype chain until reaching `null`, then compares the original object's prototype against this terminal value. Since `Object.create(null)` has no prototype (strictly `null`), it satisfies the condition `Object.getPrototypeOf(obj) === null`, returning `true`.

### Can I dispatch actions without using isPlainObject validation?

You cannot bypass `isPlainObject` validation in the standard `dispatch` implementation in [`src/createStore.ts`](https://github.com/reduxjs/redux/blob/main/src/createStore.ts) without modifying the Redux source code. However, you can dispatch non-plain objects by using **middleware** such as `redux-thunk` or `redux-saga`, which intercepts the dispatch process before it reaches the store's validation logic. These middleware packages transform functions or promises into plain objects that satisfy the `isPlainObject` check by the time they reach the reducers.