diff --git a/modules/order/src/CommerceOrderServiceProvider.php b/modules/order/src/CommerceOrderServiceProvider.php
index 3b8ec6d662eba21c29d53a32d3c119c02d3666c0..458f60defd7ae647c76a5934152c0667f664fe3c 100644
--- a/modules/order/src/CommerceOrderServiceProvider.php
+++ b/modules/order/src/CommerceOrderServiceProvider.php
@@ -2,9 +2,11 @@
 
 namespace Drupal\commerce_order;
 
+use Drupal\commerce_order\Normalizer\AdjustmentItemNormalizer;
 use Drupal\Core\DependencyInjection\ContainerBuilder;
 use Drupal\Core\DependencyInjection\ServiceProviderBase;
 use Drupal\commerce_order\DependencyInjection\Compiler\PriceCalculatorPass;
+use Symfony\Component\DependencyInjection\Reference;
 
 /**
  * Registers the PriceCalculator compiler pass.
@@ -16,6 +18,12 @@ class CommerceOrderServiceProvider extends ServiceProviderBase {
    */
   public function register(ContainerBuilder $container) {
     $container->addCompilerPass(new PriceCalculatorPass());
+    $modules = $container->getParameter('container.modules');
+    if (isset($modules['serialization'])) {
+      $container->register('commerce_order.normalizer.adjustment_item', AdjustmentItemNormalizer::class)
+        ->addArgument(new Reference('commerce_price.currency_formatter'))
+        ->addTag('normalizer', ['priority' => 20]);
+    }
   }
 
 }
diff --git a/modules/order/src/Normalizer/AdjustmentItemNormalizer.php b/modules/order/src/Normalizer/AdjustmentItemNormalizer.php
new file mode 100644
index 0000000000000000000000000000000000000000..cef5066f93ce1c79b92537884897048577d53dd2
--- /dev/null
+++ b/modules/order/src/Normalizer/AdjustmentItemNormalizer.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\commerce_order\Normalizer;
+
+use CommerceGuys\Intl\Formatter\CurrencyFormatterInterface;
+use Drupal\commerce_order\Plugin\DataType\AdjustmentItem as AdjustmentItemDataType;
+use Drupal\serialization\Normalizer\NormalizerBase;
+
+class AdjustmentItemNormalizer extends NormalizerBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $supportedInterfaceOrClass = AdjustmentItemDataType::class;
+
+  /**
+   * AdjustmentItemNormalizer constructor.
+   *
+   * @param \CommerceGuys\Intl\Formatter\CurrencyFormatterInterface $currencyFormatter
+   *   The currency formatter.
+   */
+  public function __construct(protected CurrencyFormatterInterface $currencyFormatter) {}
+
+  /**
+   * {@inheritdoc}
+   */
+  public function normalize($object, $format = NULL, array $context = []): array | bool | string | int | float | null | \ArrayObject {
+    assert($object instanceof AdjustmentItemDataType);
+    $adjustment_array = $object->getValue()->toArray();
+    $amount = &$adjustment_array['amount'];
+    $formatted_price = $this->currencyFormatter->format($amount->getNumber(), $amount->getCurrencyCode());
+    $amount = $amount->toArray();
+    $amount['formatted'] = $formatted_price;
+    return $adjustment_array;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getSupportedTypes(?string $format): array {
+    return [AdjustmentItemDataType::class => TRUE];
+  }
+
+}
diff --git a/modules/order/src/Plugin/DataType/AdjustmentItem.php b/modules/order/src/Plugin/DataType/AdjustmentItem.php
new file mode 100644
index 0000000000000000000000000000000000000000..bb42c8079a568f24b18d389bc213b85530dc0378
--- /dev/null
+++ b/modules/order/src/Plugin/DataType/AdjustmentItem.php
@@ -0,0 +1,25 @@
+<?php
+
+namespace Drupal\commerce_order\Plugin\DataType;
+
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * Defines a data type for adjustment value.
+ */
+#[DataType(
+  id: "adjustment_item",
+  label: new TranslatableMarkup('Adjustment item'),
+)]
+final class AdjustmentItem extends TypedData {
+
+  /**
+   * The data value.
+   *
+   * @var \Drupal\commerce_order\Adjustment
+   */
+  protected $value;
+
+}
diff --git a/modules/order/src/Plugin/DataType/AdjustmentProperty.php b/modules/order/src/Plugin/DataType/AdjustmentProperty.php
new file mode 100644
index 0000000000000000000000000000000000000000..c0a5a2dae16a34d715ab802f98bccd606e5e71b3
--- /dev/null
+++ b/modules/order/src/Plugin/DataType/AdjustmentProperty.php
@@ -0,0 +1,43 @@
+<?php
+
+namespace Drupal\commerce_order\Plugin\DataType;
+
+use Drupal\commerce_order\Adjustment;
+use Drupal\commerce_order\Plugin\Field\FieldType\AdjustmentItem;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\TypedData\Attribute\DataType;
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * Defines a data type for adjustments.
+ */
+#[DataType(
+  id: "adjustment_property",
+  label: new TranslatableMarkup('Adjustment property'),
+)]
+final class AdjustmentProperty extends TypedData {
+
+  /**
+   * The data value.
+   *
+   * @var \Drupal\commerce_order\Adjustment
+   */
+  protected $value;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($value, $notify = TRUE): void {
+    $parent = $this->getParent();
+    if (!($parent instanceof AdjustmentItem) || $parent->isEmpty()) {
+      return;
+    }
+    $parent_values = $parent->getValue();
+    $parent_value = reset($parent_values);
+    if (!($parent_value instanceof Adjustment)) {
+      return;
+    }
+    $this->value = $parent_value->toArray()[$this->getName()] ?? NULL;
+  }
+
+}
diff --git a/modules/order/src/Plugin/Field/FieldType/AdjustmentItem.php b/modules/order/src/Plugin/Field/FieldType/AdjustmentItem.php
index e382f327dbe446a278f7a1c6d741cebe7e19abc3..5d7e32e4722211016c8cb6595deab2583b246115 100644
--- a/modules/order/src/Plugin/Field/FieldType/AdjustmentItem.php
+++ b/modules/order/src/Plugin/Field/FieldType/AdjustmentItem.php
@@ -29,10 +29,38 @@ class AdjustmentItem extends FieldItemBase {
    * {@inheritdoc}
    */
   public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
-    $properties['value'] = DataDefinition::create('any')
+    $properties['value'] = DataDefinition::create('adjustment_item')
       ->setLabel(t('Value'))
       ->setRequired(TRUE);
 
+    $properties['type'] = DataDefinition::create('adjustment_property')
+      ->setLabel(t('Type'))
+      ->setComputed(TRUE);
+
+    $properties['label'] = DataDefinition::create('adjustment_property')
+      ->setLabel(t('Label'))
+      ->setComputed(TRUE);
+
+    $properties['amount'] = DataDefinition::create('adjustment_property')
+      ->setLabel(t('Amount'))
+      ->setComputed(TRUE);
+
+    $properties['source_id'] = DataDefinition::create('adjustment_property')
+      ->setLabel(t('Source ID'))
+      ->setComputed(TRUE);
+
+    $properties['percentage'] = DataDefinition::create('adjustment_property')
+      ->setLabel(t('Amount'))
+      ->setComputed(TRUE);
+
+    $properties['included'] = DataDefinition::create('adjustment_property')
+      ->setLabel(t('Included'))
+      ->setComputed(TRUE);
+
+    $properties['locked'] = DataDefinition::create('adjustment_property')
+      ->setLabel(t('Locked'))
+      ->setComputed(TRUE);
+
     return $properties;
   }
 
diff --git a/modules/order/tests/src/Functional/Jsonapi/OrderResourceTest.php b/modules/order/tests/src/Functional/Jsonapi/OrderResourceTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..2ecbef2013641fdb3f6fe71f9303286cb36e61cd
--- /dev/null
+++ b/modules/order/tests/src/Functional/Jsonapi/OrderResourceTest.php
@@ -0,0 +1,466 @@
+<?php
+
+namespace Drupal\Tests\commerce_order\Functional\Jsonapi;
+
+use Drupal\commerce_order\Adjustment;
+use Drupal\commerce_order\Entity\Order;
+use Drupal\commerce_order\Entity\OrderInterface;
+use Drupal\commerce_order\Entity\OrderItem;
+use Drupal\commerce_order\Entity\OrderType;
+use Drupal\commerce_price\Comparator\NumberComparator;
+use Drupal\commerce_price\Comparator\PriceComparator;
+use Drupal\commerce_price\Price;
+use Drupal\commerce_product\Entity\Product;
+use Drupal\commerce_product\Entity\ProductVariation;
+use Drupal\commerce_store\Entity\StoreInterface;
+use Drupal\commerce_store\StoreCreationTrait;
+use Drupal\Core\Datetime\DrupalDateTime;
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\Url;
+use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
+use Drupal\jsonapi\CacheableResourceResponse;
+use Drupal\Tests\jsonapi\Functional\ResourceTestBase;
+use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
+use SebastianBergmann\Comparator\Factory as PhpUnitComparatorFactory;
+
+/**
+ * JSON:API resource test for orders.
+ *
+ * @group commerce
+ */
+class OrderResourceTest extends ResourceTestBase {
+
+  use CommonCollectionFilterAccessTestPatternsTrait;
+  use StoreCreationTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $patchProtectedFieldNames = [
+    'changed' => NULL,
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = [
+    'path',
+    'commerce',
+    'commerce_store',
+    'commerce_price',
+    'commerce_product',
+    'commerce_order',
+    'serialization',
+  ];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $entityTypeId = 'commerce_order';
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $resourceTypeName = 'commerce_order--default';
+
+  /**
+   * The default store for test.
+   */
+  protected StoreInterface $store;
+
+  /**
+   * The test entity.
+   *
+   * @var \Drupal\commerce_order\Entity\OrderInterface
+   */
+  protected $entity;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setUp(): void {
+    parent::setUp();
+    $factory = PhpUnitComparatorFactory::getInstance();
+    $factory->register(new NumberComparator());
+    $factory->register(new PriceComparator());
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function createEntity() {
+    $this->store = $this->createStore();
+
+    $product = Product::create([
+      'type' => 'default',
+      'title' => $this->randomMachineName(),
+      'stores' => [$this->store],
+    ]);
+    $product->save();
+
+    $variation = ProductVariation::create([
+      'type' => 'default',
+      'sku' => '2N2NUM',
+      'product_id' => $product->id(),
+      'price' => new Price('4.50', 'USD'),
+    ]);
+    $variation->save();
+
+    /** @var \Drupal\commerce_order\Entity\OrderItemInterface $order_item */
+    $order_item = OrderItem::create([
+      'type' => 'default',
+      'quantity' => '1',
+      'unit_price' => $variation->getPrice(),
+      'purchased_entity' => $variation->id(),
+    ]);
+
+    $order = Order::create([
+      'type' => 'default',
+      'store_id' => $this->store->id(),
+      'state' => 'draft',
+      'uid' => $this->account->id(),
+    ]);
+    $order->addAdjustment(new Adjustment([
+      'type' => 'custom',
+      'label' => 'Custom adjustment for order',
+      'amount' => new Price('5.00', 'USD'),
+      'source_id' => $this->randomMachineName(),
+    ]));
+    $order->addItem($order_item);
+    $order->save();
+    return $order;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedDocument() {
+    $base_url = Url::fromUri('base:/jsonapi/commerce_order/default/' . $this->entity->uuid())
+      ->setAbsolute();
+    $customer = $this->entity->getCustomer();
+
+    // Generate created and changed times.
+    $timezone = new \DateTimeZone(DateTimeItemInterface::STORAGE_TIMEZONE);
+    $created = DrupalDateTime::createFromTimestamp($this->entity->getCreatedTime())
+      ->setTimezone($timezone)->format(\DateTime::RFC3339);
+    $changed = DrupalDateTime::createFromTimestamp($this->entity->getChangedTime())
+      ->setTimezone($timezone)->format(\DateTime::RFC3339);
+
+    // Generate order items data.
+    $order_items_data = [];
+    foreach ($this->entity->getItems() as $item) {
+      $order_items_data[] = [
+        'type' => 'commerce_order_item--default',
+        'id' => $item->uuid(),
+        'meta' => [
+          'drupal_internal__target_id' => $item->id(),
+        ],
+      ];
+    }
+
+    $options = ['currency_display' => 'symbol'];
+    $currency_formatter = $this->container->get('commerce_price.currency_formatter');
+
+    // Generate adjustments data.
+    $adjustments = [];
+    foreach ($this->entity->getAdjustments() as $adjustment) {
+      $amount_price = $adjustment->getAmount();
+      $amount = $amount_price->toArray();
+      $amount['formatted'] = $currency_formatter->format($amount_price->getNumber(), $amount_price->getCurrencyCode(), $options);
+      $adjustments[] = [
+        'amount' => $amount,
+        'included' => $adjustment->isIncluded(),
+        'label' => $adjustment->getLabel(),
+        'locked' => $adjustment->isLocked(),
+        'percentage' => $adjustment->getPercentage(),
+        'source_id' => $adjustment->getSourceId(),
+        'type' => $adjustment->getType(),
+      ];
+    }
+
+    // Generate prices data.
+    // Price and Balance do not have trailing zeroes, but in JSON:API response
+    // it has, so we need to modify the number and add missing numbers.
+    $total_price = $this->entity->getTotalPrice();
+    [$whole, $decimal] = explode('.', $total_price->getNumber());
+    $decimal = str_pad($decimal, 6, '0');
+    $total_price_data = [
+      'number' => "$whole.$decimal",
+      'currency_code' => $total_price->getCurrencyCode(),
+      'formatted' => $currency_formatter->format($total_price->getNumber(), $total_price->getCurrencyCode(), $options),
+    ];
+
+    $balance = $this->entity->getBalance();
+    [$whole, $decimal] = explode('.', $balance->getNumber());
+    $decimal = str_pad($decimal, 6, '0');
+    $balance_data = [
+      'number' => "$whole.$decimal",
+      'currency_code' => $balance->getCurrencyCode(),
+      'formatted' => $currency_formatter->format($balance->getNumber(), $balance->getCurrencyCode(), $options),
+    ];
+
+    return [
+      'jsonapi' => [
+        'version' => '1.0',
+        'meta' => [
+          'links' => [
+            'self' => ['href' => 'http://jsonapi.org/format/1.0/'],
+          ],
+        ],
+      ],
+      'data' => [
+        'type' => 'commerce_order--default',
+        'id' => $this->entity->uuid(),
+        'links' => [
+          'self' => ['href' => $base_url->toString()],
+        ],
+        'attributes' => [
+          'drupal_internal__order_id' => $this->entity->id(),
+          'order_number' => NULL,
+          'version' => $this->entity->getVersion(),
+          'mail' => $customer->getEmail(),
+          'ip_address' => '127.0.0.1',
+          'adjustments' => $adjustments,
+          'total_price' => $total_price_data,
+          'total_paid' => NULL,
+          'balance' => $balance_data,
+          'state' => 'draft',
+          'data' => [
+            'paid_event_dispatched' => FALSE,
+          ],
+          'locked' => FALSE,
+          'created' => $created,
+          'changed' => $changed,
+          'placed' => NULL,
+          'completed' => NULL,
+          'customer_comments' => $this->entity->getCustomerComments(),
+        ],
+        'relationships' => [
+          'commerce_order_type' => [
+            'data' => [
+              'type' => 'commerce_order_type--commerce_order_type',
+              'id' => OrderType::load('default')->uuid(),
+              'meta' => [
+                'drupal_internal__target_id' => 'default',
+              ],
+            ],
+            'links' => [
+              'related' => ['href' => $base_url->toString() . '/commerce_order_type'],
+              'self' => ['href' => $base_url->toString() . '/relationships/commerce_order_type'],
+            ],
+          ],
+          'store_id' => [
+            'data' => [
+              'type' => 'commerce_store--online',
+              'id' => $this->store->uuid(),
+              'meta' => [
+                'drupal_internal__target_id' => (int) $this->store->id(),
+              ],
+            ],
+            'links' => [
+              'related' => ['href' => $base_url->toString() . '/store_id'],
+              'self' => ['href' => $base_url->toString() . '/relationships/store_id'],
+            ],
+          ],
+          'uid' => [
+            'data' => [
+              'type' => 'user--user',
+              'id' => $customer->uuid(),
+              'meta' => [
+                'drupal_internal__target_id' => $customer->id(),
+              ],
+            ],
+            'links' => [
+              'related' => ['href' => $base_url->toString() . '/uid'],
+              'self' => ['href' => $base_url->toString() . '/relationships/uid'],
+            ],
+          ],
+          'billing_profile' => [
+            'data' => NULL,
+            'links' => [
+              'related' => ['href' => $base_url->toString() . '/billing_profile'],
+              'self' => ['href' => $base_url->toString() . '/relationships/billing_profile'],
+            ],
+          ],
+          'order_items' => [
+            'data' => !empty($order_items_data) ? $order_items_data : NULL,
+            'links' => [
+              'related' => ['href' => $base_url->toString() . '/order_items'],
+              'self' => ['href' => $base_url->toString() . '/relationships/order_items'],
+            ],
+          ],
+        ],
+      ],
+      'links' => [
+        'self' => ['href' => $base_url->toString()],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getPostDocument() {
+    return [
+      'data' => [
+        'type' => 'commerce_order--default',
+        'attributes' => [
+          'order_number' => '#1',
+        ],
+        'relationships' => [
+          'store_id' => [
+            'data' => [
+              'type' => 'commerce_store--online',
+              'id' => $this->store->uuid(),
+              'meta' => [
+                'drupal_internal__target_id' => (int) $this->store->id(),
+              ],
+            ],
+          ],
+        ],
+      ],
+    ];
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function setUpAuthorization($method) {
+    switch ($method) {
+      case 'GET':
+        $this->grantPermissionsToTestedRole(['view default commerce_order']);
+        break;
+
+      case 'DELETE':
+        $this->grantPermissionsToTestedRole(['delete default commerce_order']);
+        break;
+
+      case 'POST':
+        $this->grantPermissionsToTestedRole([
+          'view commerce_store',
+          'create default commerce_order',
+        ]);
+        break;
+
+      case 'PATCH':
+        $this->grantPermissionsToTestedRole(['update default commerce_order']);
+        break;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessMessage($method) {
+    return match ($method) {
+      'GET' => "The 'view own commerce_order' permission is required.",
+      'DELETE' => "The following permissions are required: 'delete commerce_order' OR 'delete default commerce_order'.",
+      'POST' => "The following permissions are required: 'administer commerce_order' OR 'create commerce_order' OR 'create default commerce_order'.",
+      'PATCH' => "The following permissions are required: 'update commerce_order' OR 'update default commerce_order'.",
+      default => parent::getExpectedUnauthorizedAccessMessage($method),
+    };
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedUnauthorizedAccessCacheability() {
+    $cacheability = parent::getExpectedUnauthorizedAccessCacheability();
+    $cacheability->addCacheableDependency($this->entity);
+    $contexts = array_map(function ($context) {
+      if ($context === 'user.permissions') {
+        $context = 'user';
+      }
+      return $context;
+    }, $cacheability->getCacheContexts());
+    $cacheability->setCacheContexts($contexts);
+    return $cacheability;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, ?array $sparse_fieldset = NULL, $filtered = FALSE) {
+    $cacheability = parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered);
+
+    // Modify cache tags for collection request.
+    $tags = $cacheability->getCacheTags();
+    foreach ($collection as $entity) {
+      if (!$entity->access('view', $account, TRUE)->isAllowed()) {
+        $tag = "{$entity->getEntityTypeId()}:{$entity->id()}";
+        $key = array_search($tag, $tags, TRUE);
+        unset($tags[$key]);
+      }
+    }
+    $cacheability->setCacheTags($tags);
+
+    // Modify cache contexts for collection request.
+    $contexts = array_map(function ($context) {
+      if ($context === 'user') {
+        $context = 'user.permissions';
+      }
+      return $context;
+    }, $cacheability->getCacheContexts());
+    $contexts = array_unique($contexts);
+    $cacheability->setCacheContexts($contexts);
+
+    return $cacheability;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getExpectedCollectionResponse(array $collection, $self_link, array $request_options, ?array $included_paths = NULL, $filtered = FALSE) {
+    $response = parent::getExpectedCollectionResponse($collection, $self_link, $request_options, $included_paths, $filtered);
+    $document = $response->getResponseData();
+
+    // Actual response does not have omitted message except when "included"
+    // presented in query.
+    if (!isset($document['included'])) {
+      unset($document['meta']['omitted']);
+      if (empty($document['meta'])) {
+        unset($document['meta']);
+      }
+    }
+    $cacheability = $response->getCacheableMetadata();
+    return (new CacheableResourceResponse($document, 200))->addCacheableDependency($cacheability);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function getRelationshipFieldNames(?EntityInterface $entity = NULL) {
+    $entity = $entity ?: $this->entity;
+
+    // Remove non-existing fields.
+    $field_names = array_map(function ($field_name) {
+      $replacement = 'drupal_internal__';
+      if (str_starts_with($field_name, $replacement)) {
+        $field_name = str_replace($replacement, '', $field_name);
+      }
+      return $field_name;
+    }, parent::getRelationshipFieldNames($entity));
+
+    return array_filter($field_names, function ($field_name) use ($entity) {
+      return $entity->hasField($field_name);
+    });
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function entityLoadUnchanged($id) {
+    $entity = parent::entityLoadUnchanged($id);
+    if ($entity instanceof OrderInterface) {
+      $entity->recalculateTotalPrice();
+    }
+    return $entity;
+  }
+
+}