I'm not sure if I'm reporting this bug in the correct place, but I don't know where else to report it...

I have a custom entity type called 'visit' with a 'datetime' base field, like so:

$fields['date'] = BaseFieldDefinition::create('datetime')
  ->setLabel(t('Date'))
  ->setRequired(TRUE);

When I create a Visit entity with a date value, the UTC version of that date is stored correctly in the database. For example, "2018-09-16T12:00:00".

When I programmatically load this Visit entity and inspect the DateTime object, it shows the correct datetime string and time zone (UTC):

dpm(entity_load('visit', 1)->date[0]->date->getPhpDateTime());
// date = 2018-09-16T12:00:00+00:00
// timezone = UTC

When I visit the JSON API URL for this entity, I'm expecting to see the formatted datetime string with either the UTC offset, or my localized (America/Chicago) offset and a different time. So, one of the following would be correct:

  • 2018-09-16T12:00:00+00:00
  • 2018-09-16T07:00:00-05:00

However, what I actually see is the datetime string with the wrong timezone offset, or rather the correct timezone offset but the wrong datetime string:

/jsonapi/visit/visit/3d3f2b56-0789-436a-addd-2e2cb0271796
data.attributes.date = "2018-09-16T12:00:00-05:00"
expected = "2018-09-16T07:00:00-05:00"

I'm not sure if the culprit is with JSON API or elsewhere, but it seems that somewhere the DateTime object is being loaded from storage with my local timezone (instead of UTC).

In Drupal\jsonapi\ForwardCompatibility\Normalizer\DateTimeNormalizer::normalize(), if I inspect $datetime->getDateTime(), I see that it has the wrong combination of datetime string and timezone offset.

Any idea what could be causing this?

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

joelstein created an issue. See original summary.

joelstein’s picture

Issue summary: View changes

For what it's worth, if I patch Drupal core with #2926508-115: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API to enhance the core Rest API so that it includes datetime + timezone offset strings, the same issue occurs.

/entity/visit/1?_format=json
date.value = "2018-09-16T12:00:00-05:00"
expected = "2018-09-16T07:00:00-05:00"
gabesullice’s picture

Assigned: Unassigned » Wim Leers
Status: Active » Postponed (maintainer needs more info)
Issue tags: +API-First Initiative

Hi @joelstein! First, thank you for such a detailed bug report, that's always a pleasant surprise :)

TBH, I'm not very familiar with this topic. @Wim Leers spearheaded a lot of DateTime improvements in core and in JSON API. I think he'll be the best maintainer to help you with this, but I know he's on vacation ATM.

I have just a few questions for which I'm sure he'd like to know the answers:

  • Are you on Drupal 8.6.1?
  • Are you on JSON API 2.0-beta1? Have you tried 2.x-dev?
  • Are you using JSON API Extras? If so, are you using the latest version? If so, are you using any field enhancers? If so, are they related to these date fields?

Thanks again for the awesome report. When you've answered these questions, please set the status back to "Active"

joelstein’s picture

Status: Postponed (maintainer needs more info) » Active

Thank you for the timely follow-up! I appreciate it. :)

Yes, I'm using Drupal 8.6.1, JSON API 2.0-beta1 (haven't tried with 2.x-dev), not using JSON API Extras.

Another FWIW: I created a basic "Event" content type with a datetime field, this is actually easier to reproduce. When I visit the JSON API endpoint for that event node, the same issue occurs. Is this happening for anyone else?

/jsonapi/node/event/39a6caf7-6981-4de5-9cbb-f45a27a92737
date.value = "2018-09-16T12:00:00-05:00"
expected = "2018-09-16T07:00:00-05:00"

I'm digging deeper on this and what I'm currently seeing is that in DateTimeIso8601Normalizer and DateTimeNormalizer we're getting the DateTime object via $datetime->getDateTime(), which returns a DrupalDateTime object. The problem is that only the datetime string value from the database (without any timezone information) is sent to DrupalDateTime::__construct() ("2018-09-16T12:00:00"), and thus DrupalDateTime (actually DateTimePlus) doesn't have any timezone information and so it assumes the local timezone.

Somehow we need to inform this whole thing that we have a database value which is in the UTC timezone...

joelstein’s picture

Okay, so yeah, the normalizers (DateTimeIso8601Normalizer and DateTimeNormalizer) are asking DateTimeIso8601 to give them a DrupalDateTime object in the correct timezone, but DateTimeIso8601 is not informing DrupalDateTime of the source timezone (in this case at least, it's "UTC", because that's how Drupal store's datetime fields).

Here's a simple patch which helps fix this in the ForwardCompatibility classes with JSON API. I'm also posting the corresponding patch which would affect #2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API, but keeping it here for now to isolate the conversation at this point.

We "could" simply patch DateTimeIso8601::getDateTime() to create a DrupalDateTime from a UTC string. That would solve this issue, but may introduce other problems... I don't have a broad enough knowledge of how everything works to feel confident about this solution.

I'm going to use these patches for a bit and stop further troubleshooting until somebody else weighs in.

joelstein’s picture

Oops, I had one small typo in the last patches.

joelstein’s picture

More work is needed here. It should change anything other than datetime_iso8601 fields (not any other datetime fields). Because as it's written in my patches, it breaks timestamp fields. Whoops.

I found some other issues which may be at play here.

#2716891: DateTimeIso8601::setDateTime() vs. DateTimeItem::schema(): (at least) one of them is broken: this issue works changes the storage format for datetime fields to 25 characters to allow for the timezone offset in storage, which is a really good idea, because then when it's loaded, we know something about the date (instead of assuming incorrectly that it's a local date).

#2632040: [PP-1] Add ability to select a timezone for datetime field: adds a separate column for datetime tables to store the timezone. Looks like a lot of work.

I believe the simplest solution, currently, is to append "+00:00" to the datetime value when it's loaded from the database. Is this possible somehow?

Wim Leers’s picture

Title: Datetime field shown with wrong timezone offset » [PP-1] [upstream] Datetime field shown with wrong timezone offset
Component: Miscellaneous » Code
Assigned: Wim Leers » Unassigned
Status: Needs work » Postponed

Thank you very much for the detailed report and especially your own digging! 👌 🙏

To be frank: Drupal's datetime field is sort of a mess, due to its historical roots, and due to PHP and MySQL not dealing with dates, time, and timezones very well, at least in the past…


When I create a Visit entity with a date value, the UTC version of that date is stored correctly in the database. For example, "2018-09-16T12:00:00".

Off the top of my head: that's not storing the date in UTC though. Well, the top of my head was wrong, because \Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface::STORAGE_TIMEZONE definitely says something else.

So I dug in and followed your steps to reproduce. I did manage to reproduce this. This indeed means that #2926508: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API also is missing some test coverage and needs further work. So … I did that work :) See #2926508-136: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API and #2926508-137: Add DateTimeNormalizer+TimestampNormalizer, deprecate TimestampItemNormalizer: @DataType-level normalizers are reusable by JSON:API. AFAICT this needs to be fixed in core, so marking postponed on upstream blocker.

Thanks again for reporting this!

joelstein’s picture

Wim, thanks for the kind words. This is a super complicated issue, because anything having to do with datetime, timezones, db storage, normalization and denormalization is a mess. Technical soup!

Here's what I came up with for the project we're working on, just so we could move forward. I'm pasting it here in case it's useful for this and the Drupal core issues. It's just a custom normalizer in a custom module which fills in the gaps for datetime field handling.

Note: there are probably issues with the date-only handling, which I didn't test, but this works pretty well for datetime fields.

Also note: in my denormalize function I return a string formatted according to how the DateTimeItem fields are stored in the database. When I returned a \DateTime object, all sorts of problems ensued. This approached seems to work better (for me, at least).

Thanks for everything!

mymodule.services.yml:

mymodule.normalizer.datetimeiso8601:
    class: \Drupal\mymodule\Normalizer\DateTimeIso8601Normalizer
    tags:
      # Higher priority than serializer.normalizer.datetimeiso8601.jsonapi.
      - { name: normalizer, priority: 21 }

mymodule/src/Normalizer/DateTimeIso8601Normalizer.php:

namespace Drupal\mymodule\Normalizer;

use Drupal\Core\Datetime\DrupalDateTime;
use Drupal\Core\TypedData\Plugin\DataType\DateTimeIso8601;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItem;
use Drupal\datetime\Plugin\Field\FieldType\DateTimeItemInterface;
use Drupal\jsonapi\ForwardCompatibility\Normalizer\DateTimeNormalizer;

/**
 * There are several problems with Drupal's datetime-field handling, how it's
 * stored, how it's normalized, how it's denormalized, etc.
 *
 * This class borrows from the similar JSON API class, which itself is a
 * forward-port of #2926508. Still, there are other issues (notably #2716891).
 *
 * I don't like the idea of a moving target when it comes to API. So I have
 * this simple class to normalize outgoing dates to UTC format (working around
 * issues with the DateTimeIso8601 class), and denormalize incoming dates so
 * they can be stored without timezone data in the database. Sigh.
 *
 * @see https://www.drupal.org/project/drupal/issues/2926508
 * @see https://www.drupal.org/project/drupal/issues/2716891
 * @see https://www.drupal.org/project/jsonapi/issues/2999438
 * @see https://www.drupal.org/project/drupal/issues/2632040
 */
class DateTimeIso8601Normalizer extends DateTimeNormalizer {

  /**
   * {@inheritdoc}
   */
  protected $supportedInterfaceOrClass = DateTimeIso8601::class;

  /**
   * {@inheritdoc}
   */
  public function normalize($datetime, $format = NULL, array $context = []) {
    // Always normalize in UTC timezone.
    $drupal_date_time = new DrupalDateTime($datetime->getCastedValue(), 'UTC');

    // Date-only fields.
    $field_item = $datetime->getParent();
    if ($field_item instanceof DateTimeItem && $field_item->getFieldDefinition()->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
      return $drupal_date_time->format('Y-m-d');
    }

    // Datetime fields.
    return $drupal_date_time->format(\DateTime::RFC3339);
  }

  /**
   * {@inheritdoc}
   */
  public function denormalize($data, $class, $format = NULL, array $context = []) {
    // Date-only fields.
    if ($context['field_type'] === 'datetime' && $context['field_definition']->getFieldStorageDefinition()->getSetting('datetime_type') === DateTimeItem::DATETIME_TYPE_DATE) {
      $this->allowedFormats = ['date-only' => 'Y-m-d'];
      return parent::denormalize($data, $class, $format, $context)
        ->setTimezone(new \DateTimeZone('UTC'))
        ->format(DateTimeItemInterface::DATE_STORAGE_FORMAT);
    }

    // Datetime fields.
    return parent::denormalize($data, $class, $format, $context)
      ->setTimezone(new \DateTimeZone('UTC'))
      ->format(DateTimeItemInterface::DATETIME_STORAGE_FORMAT);
  }

}
joshua.boltz’s picture

I've run into the issue where the date output in JSON API is different from the date stored in the Drupal field.
I applied this patch, but when i view the JSON API, i get this error:

"Exception: DateTime object not set. in /home/vagrant/docroot/web/core/lib/Drupal/Component/Datetime/DateTimePlus.php

I've also tried the patch in https://www.drupal.org/project/drupal/issues/3002164, which does not give an error, but also does not fix the date output.

joshua.boltz’s picture

Attached are visuals that show the issue.

Wim Leers’s picture

#12 is not necessarily wrong. To be able to answer whether that's a bug or not, I'd need to know:

  1. what the site's default timezone is
  2. what the timezone preference is of the user as whom you accessed the form
  3. what the timezone preference is of the user as whom you talked to JSON API
Wim Leers’s picture

Title: [PP-1] [upstream] Datetime field shown with wrong timezone offset » [upstream] Datetime field shown with wrong timezone offset
Version: 8.x-2.0-beta1 » 8.x-2.0-rc3
Assigned: Unassigned » Wim Leers
Status: Postponed » Needs work

#3002164: DateTimeIso8601::getDateTime() does not specify the storage timezone when constructing DrupalDateTime object landed in Drupal 8.7 two days ago! Now I just need to backport it to the JSON:API module's "forward compatibility" layer. Will do that on Monday.

Thanks again for reporting this, @joelstein!

Wim Leers’s picture

Version: 8.x-2.0-rc3 » 8.x-2.x-dev
joelstein’s picture

Great! Thanks so much, Wim!

Wim Leers’s picture

Status: Needs work » Needs review
FileSize
2.46 KB

(Sorry, bit later than intended due to last week's security release.)

First, adding explicit test coverage for this. This should pass on Drupal 8.7 without changes (since that branch of core includes #3002164: DateTimeIso8601::getDateTime() does not specify the storage timezone when constructing DrupalDateTime object), but fail on 8.6 and 8.5.

Wim Leers’s picture

Applying @joelstein's patch from #6 fixes it. (The interdiff here is identical to his patch in #6.) This should pass on all branches.

The last submitted patch, 17: 2999438-17.patch, failed testing. View results

Wim Leers’s picture

Failed and passed as predicted! 👍

+++ b/src/ForwardCompatibility/Normalizer/DateTimeIso8601Normalizer.php
@@ -37,7 +37,7 @@ class DateTimeIso8601Normalizer extends DateTimeNormalizer {
-      return $datetime->getDateTime()->format($this->allowedFormats['date-only']);
+      return $this->getDateTime($datetime)->format($this->allowedFormats['date-only']);

+++ b/src/ForwardCompatibility/Normalizer/DateTimeNormalizer.php
@@ -40,7 +41,7 @@ class DateTimeNormalizer extends NormalizerBase implements DenormalizerInterface
-    return $datetime->getDateTime()
+    return $this->getDateTime($datetime)

@@ -51,6 +52,18 @@ class DateTimeNormalizer extends NormalizerBase implements DenormalizerInterface
+  protected function getDateTime($datetime) {
+    return new DrupalDateTime($datetime->getValue(), 'UTC');
+  }

The problem with this solution is that it makes it hard to drop it in the future when Drupal core 8.5 and 8.6 are no longer supported.

So we should refactor this so that this temporary work-around becomes easy to remove.

Wim Leers’s picture

Assigned: Wim Leers » Unassigned
Status: Needs review » Reviewed & tested by the community
  1. Failing test coverage. ✅
  2. Green on 8.5 + 8.6 + 8.7 ✅
  3. Easy to remove 8.5 + 8.6 BC layer. ✅
Wim Leers’s picture

+++ b/src/ForwardCompatibility/Normalizer/DateTimeIso8601Normalizer.php
@@ -37,7 +38,11 @@ class DateTimeIso8601Normalizer extends DateTimeNormalizer {
+      $drupal_date_time = floatval(floatval(\Drupal::VERSION) >= 8.7)
+        ? $datetime->getDateTime()
+        : new DrupalDateTime($datetime->getValue(), 'UTC');

The fallback behaves slightly different than \Drupal\Core\TypedData\Type\DateTimeInterface::getDateTime() dictates: it always returns a DrupalDateTime object, but it should return NULL in case there is no date (in case getValue() is empty).

Fixing that nit.

(Discovered this while working on #3017945: [upstream] Field types with a required property *and* an optional @DataType=datetime_iso8601 property trigger fatal error in DateTimeNormalizer when optional property is empty.)

  • Wim Leers committed 363b639 on 8.x-2.x
    Issue #2999438 by Wim Leers, joelstein, joshua.boltz: [upstream]...
Wim Leers’s picture

Status: Reviewed & tested by the community » Fixed

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.

rpayanm’s picture

Project: JSON:API » Drupal core
Version: 8.x-2.x-dev » 8.9.x-dev
Component: Code » jsonapi.module

Moving to Drupal core's issue queue.

I'm working on https://www.drupal.org/project/drupal/issues/3122113