-4

I have this Joda-Time date which I want to migrate to java.time.OffsetDateTime:

import org.joda.time.DateTime;
private DateTime createdDate;

private String creationTime;
.....
DateTimeFormatter dtf = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");

...
.setCreationTime(dtf.print(account.getCreatedDate().toDateTime(DateTimeZone.UTC)));

As you can tell, the code gets the Joda-Time DateTime named createdDate from the object account, converts it to a DateTime in UTC, formats it to a String using the formatter and sets creationTime of an object to the resulting String.

I tried to migrate this code to:

    import java.time.OffsetDateTime;
    private OffsetDateTime createdDate;
    
    private String creationTime;
    .....
    DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss'Z'");
    
    ... 
   .setCreationTime(dtf.format(account.getCreatedDate().withOffsetSameInstant(ZoneOffset.of("UTC"))));

I want this code to do the same as the Joda-Time code that I am migrating away from. Specifically I should get the same resulting string. The question is: Do I need to set timezone using withOffsetSameInstant(ZoneOffset.of("UTC"))));, or I can skip it?

In one example the call did not seem to make any difference.

7
  • What do you mean by "do I need to"? What is your goal? Does not doing that achieve your goal? Commented Feb 16 at 16:21
  • Do you have an example String to be parsed? Commented Feb 16 at 16:21
  • 1
    If account.getCreatedDate() is returning a string in the format of your formatter (DateTimeFormatter.ISO_INSTANT), then you can feed that directly to: OffsetDateTime.parse("2025-02-16T16:25:00Z"); Commented Feb 16 at 16:27
  • 2
    Not answering your question, you are doing it wrong with both Joda-Time and java.time. You must never, ever hardcode Z as a literal in single quotes in the format pattern. Z is an offset of 0 from UTC and needs to be parsed and formatted as an offset. Commented Feb 16 at 17:27
  • 1
    @PeterPenzov You need not bother with defining a formatting pattern for inputs that comply with the ISO 8601 standard formats used by default in java.time. See my Answer. Commented Dec 6 at 0:55

2 Answers 2

1

Use a proper date-time object for your creation time

To store your creation time in an instance field in an object, use Instant, not String. Strings are for presentation to users, for storage if your storage doesn’t support date-time types and for data exchange.

For example:

    OffsetDateTime creationDateTime = ZonedDateTime.of(
                    2025, 2, 16, 23, 35, 0, 0,
                    ZoneId.of("Asia/Yekaterinburg"))
            .toOffsetDateTime();
    System.out.println(creationDateTime);

    Instant creationTime = creationDateTime.toInstant();
    System.out.println(creationTime);

Output:

2025-02-16T23:35+05:00
2025-02-16T18:35:00Z

The Instant already prints in the format you asked for, only it will also print a fraction of second if a non-zero fraction exists.

If you need a String

If against my recommendation you insist on a string, I suggest this formatter:

private static final DateTimeFormatter formatter
        = new DateTimeFormatterBuilder()
                .append(DateTimeFormatter.ISO_LOCAL_DATE)
                .appendLiteral('T')
                .appendPattern("HH:mm:ssX")
                .toFormatter(Locale.ROOT);

It’s wordier than yours but has the advantage of reusing the built-in DateTimeFormatter.ISO_LOCAL_DATE. Now convert to UTC and format:

    String utcString = creationDateTime.withOffsetSameInstant(ZoneOffset.UTC)
            .format(formatter);
    System.out.println(utcString);

Output is the same as before:

2025-02-16T18:35:00Z

If you prefer putting the formatting the other way around, do that, it makes no difference:

    String utcString = formatter.format(
            creationDateTime.withOffsetSameInstant(ZoneOffset.UTC));
    System.out.println(utcString);

Or the short alternative: Don’t use any formatter. Discard any fraction of second from the Instant from above and use its toString method:

    String utcString = creationTime.truncatedTo(ChronoUnit.SECONDS)
            .toString();

Output is the same again.

You are not adding a time zone

An OffsetDateTime has an offset from UTC, hence the class name. In java.time parlance a time zone is a IANA time zone and comprises past, present and future UTC offsets used in a particular place, the zone, such as Asia/Yekaterinburg in my code example above. So UTC is not a time zone, and an OffsetDateTime cannot have a time zone in this sense (this is what we have the ZonedDateTime class for).

Converting to UTC

Do I need to set timezone using withOffsetSameInstant(ZoneOffset.of("UTC")))); or I can skip it?

First, ZoneOffset.of("UTC") throws an exception. Instead use the built-in constant ZoneOffset.UTC. Then the answer to your question is, it depends on the result you want, of course. Your result will typically differ depending on whether you convert it to UTC or not. In my example above the OffsetDateTime had an offset of 5 hours from UTC, which we converted to 0 hours from UTC.

Never enclose Z in single quotes in your format pattern

As @Anonymous said in a comment, you will typically be getting an incorrect result from including 'Z' (with the quotes) in a format pattern string. When you convert your date and time to UTC, the result happens to be the same. But you asked whether you needed to. What would happen if you didn’t? You would get the date and time of the original OffsetDateTime with an appended Z that would tell the reader that the date and time were in UTC. This would be doubly wrong: You would get a lie, and you would not get date and time at the desired UTC offset. Use pattern letter X as I do for the UTC offset. Now if you try leaving out the conversion to UTC, you still don’t get the date and time at the expected offset, but at least you are being told the offset so that you can see it’s not what you had wanted. If a wrong output, which do you prefer? A wrong output that looks right so you can’t tell that it’s wrong? Or an output that declares what it is giving you so you can see that it’s not what you expected? For the latter, use pattern letter X. It prints the offset as Z if it is zero hours-minutes-seconds from UTC and only in this case.

Sign up to request clarification or add additional context in comments.

Comments

1

tl;dr

Do I need to set timezone using withOffsetSameInstant(ZoneOffset.of("UTC"))));, or I can skip it?

No, you do not need to specify an offset. The Z in your input string already indicates an offset of zero.

Instant.parse( "2025-01-23T01:23:45Z" )  // That's all you need.

Generate text in standard ISO 8601 format by simply calling toString.

String output = instant.toString() ;  // Returns: 2025-01-23T01:23:45Z

Simplify your custom class to:

import java.time.Instant;
…
private Instant created;  // (A) Use `Instant` to represent a moment as seen in UTC. (B) No need for `String creationTime`. Generate text when needed.

Do not ignore Z

"yyyy-MM-dd'T'HH:mm:ss'Z'"

Never put single-quotes around Z in a date-time formatting pattern. Doing so ignores the vital meaning of that letter as an informational signal. The Z is short for an offset of zero hours-minutes-seconds, +00:00. Your quotes are deleting information.

The T in the middle should indeed have single-quotes around it. The single-quotes mean “expect but ignore the text”. The T is merely a delimiter without semantics. So we do indeed need to expect it but then want to ignore it as meaningless. Not so with Z.

ISO 8601

Your format complies with the ISO 8601 standard.

java.time.Instant

The java.time classes use standard ISO 8601 formats by default when parsing/generating text. So no need to specify a formatting pattern at all. Just call Instant.parse. That method uses the predefined formatting pattern in constant DateTimeFormatter.ISO_INSTANT.

Instant instant = Instant.parse( "2025-01-23T01:23:45Z" ) ;

UTC-time

I tried to migrate this code to:

You are workin too hard. No need for that code.

A java.time.Instant is always “in UTC”, meaning its offset is always zero, +00:00.

Your input string with a Z on the end means an offset of zero. So it parses directly as a Instant object with no adjustment to offset.

Prefer Instant over OffsetDateTime

If your goal is to represent a moment in UTC, just use Instant class. No need for OffsetDateTime. An Instant object and a java.time.OffsetDateTime object with an offset of zero (ZoneOffset.UTC), have the same meaning.

As a data field on your custom class, you should generally use Instant as the type if your intent is a moment as seen in UTC.

So likely your line private OffsetDateTime createdDate; should be private Instant createdDate;. And I recommend simple but clear variable naming. So I would shorten to private Instant created;.

JDBC

Generally, we only need OffsetDateTime for use with databases via JDBC.

JDBC 4.2 and later requires every JDBC driver to support a subset of the java.time types. The OffsetDateTime class is one of the types in that subset, but Instant (and ZonedDateTime) are not. That is because of the peculiar limitations in the SQL Standard (written by people who did not understand date-time handling).

Some JDBC drivers may offer support for Instant. You may choose to use such a feature, but know that your code may not port to other

When you need to exchange an Instant object with a database, convert to/from OffsetDateTime. For your convenience, the ZoneOffset class offers a constant for an offset of zero, ZoneOffset.UTC.

Writing to database

When writing to database, we call Instant#atOffset to convert to OffsetDateTime.

Instant created = Instant.now() ;
OffsetDateTime odt = created.atOffset( ZoneOffset.UTC ) ; 
myPreparedStatement.setObject( … , odt ) ;

Abbreviated:

myPreparedStatement.setObject( … , Instant.now().atOffset( ZoneOffset.UTC )  ) ;

Or even briefer, skip Instant to directly call OffsetDateTime.now.

myPreparedStatement.setObject( … , OffsetDateTime.now( ZoneOffset.UTC )  ) ;

⚠️ Be aware that some databases such as Postgres adjust inputs to UTC values for storage. And some do not.

Reading from database

When reading, fetch a OffsetDateTime object via JDBC, then convert to Instant.

OffsetDateTime odt = myResultSet.getObject( … , OffsetDateTime.class ) ;
Instant instant = odt.toInstant() ;

Abbreviated:

Instant instant = myResultSet.getObject( … , OffsetDateTime.class ).toInstant() ;

⚠️ Many middleware tools & SQL clients have the anti-feature of dynamically applying some default time zone after retrieving date-time values. This creates the confusing illusion that the value was stored with that time zone. JDBC clients (that I know of) do not apply some time zone. So the code shown above should always give you the truth, the actual value stored by the database.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.