Instant
, not LocalDateTime
Actually, LocalDateTime
is the wrong class to use for tracking a "when created" value. That class lacks the context of a time zone or offset-from-UTC, so it cannot represent a moment, a specific point on the timeline. A LocalDateTime
is inherently ambiguous.
Instead, use the Instant
class. This class represents a moment as seen with an offset from UTC of zero hours-minutes-seconds.
Gson adapter
I do not regularly use Gson, so I may be mistaken, but… Apparently Gson oddly still lacks support for java.time. This despite JSR 310 being adopted years ago, way back in Java 8.
To get around that lacking in Gson, You will need to either import some library that provides java.time types, or you can easily write your own adapter.
Let's start with a Person
class.
Notice how the two member fields name
& createdAt
are marked private
, as are the fields in your Question.
Also notice how this class is immutable, with getters but no setters. Gson is still able to reconstitute an object of this class via the magic of Java Reflection. This is also why you could just as well define this class as a short single-line record: public record Person ( String name , Instant createdAt ) {}
.
package work.basil.example.gson;
import java.time.Instant;
import java.util.Objects;
public final class Person
{
private final String name;
private final Instant createdAt;
public Person ( String name , Instant createdAt )
{
this.name = name;
this.createdAt = createdAt;
}
public String name ( ) { return name; }
public Instant createdAt ( ) { return createdAt; }
@Override
public boolean equals ( Object obj )
{
if ( obj == this ) { return true; }
if ( obj == null || obj.getClass() != this.getClass() ) { return false; }
var that = ( Person ) obj;
return Objects.equals( this.name , that.name ) &&
Objects.equals( this.createdAt , that.createdAt );
}
@Override
public int hashCode ( )
{
return Objects.hash( name , createdAt );
}
@Override
public String toString ( )
{
return "Person[" +
"name=" + name + ", " +
"createdAt=" + createdAt + ']';
}
}
We write an adapter for the Instant
type. We use standard ISO 8601 format for the serialized value. The Z
on the end of such a formatted string is an abbreviation of +00:00
, meaning an offset of zero hours-minutes-seconds from UTC.
package work.basil.example.gson;
import com.google.gson.TypeAdapter;
import com.google.gson.stream.JsonReader;
import com.google.gson.stream.JsonWriter;
import java.io.IOException;
import java.time.Instant;
public class Gson_InstantTypeAdapter extends TypeAdapter < Instant >
{
@Override
public void write ( JsonWriter jsonWriter , Instant instant ) throws IOException
{
jsonWriter.value( instant.toString() ); // Writes in standard ISO 8601 format.
}
@Override
public Instant read ( JsonReader jsonReader ) throws IOException
{
return Instant.parse( jsonReader.nextString() ); // Parses standard ISO 8601 format.
}
}
And we write an app to show those in action.
Notice how we register our adapter for the Instant
type.
package work.basil.example.gson;
import com.google.gson.*;
import java.time.Instant;
public class App
{
public static void main ( String[] args )
{
App app = new App();
app.demo();
}
private void demo ( )
{
Gson gson =
new GsonBuilder()
.registerTypeAdapter( Instant.class , new Gson_InstantTypeAdapter() )
.create();
// Write to JSON.
Person person = new Person( "John Doe" , Instant.now() ); //For example: 2022-02-02T11:11:510Z
String personAsJson = gson.toJson( person );
System.out.println( "personAsJson = " + personAsJson );
// Parse JSON.
Person p = gson.fromJson( personAsJson , Person.class );
System.out.println( "p.toString() = " + p );
}
}
When run:
personAsJson = {"name":"John Doe","createdAt":"2023-03-28T06:56:18.867974Z"}
p.toString() = Person[name=John Doe, createdAt=2023-03-28T06:56:18.867974Z]