Skip to content

Instantly share code, notes, and snippets.

@pavelfomin
Created June 24, 2025 19:02
Show Gist options
  • Save pavelfomin/380fec9c966428dfdd8ef4049b8386d2 to your computer and use it in GitHub Desktop.
Save pavelfomin/380fec9c966428dfdd8ef4049b8386d2 to your computer and use it in GitHub Desktop.
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import org.javers.core.ChangesByCommit;
import org.javers.core.diff.Change;
import org.javers.core.json.BasicStringTypeAdapter;
import org.javers.core.json.typeadapter.util.UtilTypeCoreAdapters;
import org.springframework.boot.autoconfigure.jackson.Jackson2ObjectMapperBuilderCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import java.time.Instant;
import java.time.temporal.ChronoField;
import java.util.List;
@Configuration
public class JaVersConfiguration {
@Bean
InstantTruncatedTypeAdapter instantTruncatedTypeAdapter() {
return new InstantTruncatedTypeAdapter();
}
/**
* The Jackson2ObjectMapperBuilder context can be customized by one or more Jackson2ObjectMapperBuilderCustomizer beans.
* Such customizer beans can be ordered (the Boot customizer has an order of 0), letting additional customization
* be applied both before and after the Boot customization.
*/
@Bean
Jackson2ObjectMapperBuilderCustomizer jacksonJaVersCustomizer() {
return (Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) ->
jacksonObjectMapperBuilder
.mixIn(ChangesByCommit.class, JaVersChangesMixIn.class)
.mixIn(Change.class, ClassNameTypeMixIn.class);
}
/**
* Truncated to microseconds {@link Instant} type adapter.
* Instant precision is 9 while PostgreSQL timestamp precision is 6 (rounded).
* See {@link org.javers.java8support.InstantTypeAdapter}.
*/
static class InstantTruncatedTypeAdapter extends BasicStringTypeAdapter<Instant> {
@Override
public String serialize(Instant sourceValue) {
return UtilTypeCoreAdapters.serialize(sourceValue
.with(ChronoField.MICRO_OF_SECOND,
Math.round((double) sourceValue.get(ChronoField.NANO_OF_SECOND) / 1000)));
}
@Override
public Instant deserialize(String serializedValue) {
return UtilTypeCoreAdapters.deserializeToInstant(serializedValue);
}
@Override
public Class<?> getValueType() {
return Instant.class;
}
}
/**
* Makes `changes` returned by non-standard `get()` method serializable by Jackson.
*/
abstract static class JaVersChangesMixIn {
@JsonProperty("changes")
abstract List<Change> get();
}
/**
* Instructs Jackson to serialize class name as `type` attribute.
* Useful for JaVers Change subclasses, especially org.javers.core.diff.changetype.NewObject.
*/
@JsonTypeInfo(use = JsonTypeInfo.Id.SIMPLE_NAME, property = "type")
abstract static class ClassNameTypeMixIn {
}
}
@pavelfomin
Copy link
Author

The following configuration is also required unless a public schema is used by the application (not likely)

javers:
  # https://github.com/javers/javers/issues/934
  sqlSchema: ${app.db.schema}

@pavelfomin
Copy link
Author

pavelfomin commented Jun 24, 2025

InstantTruncatedTypeAdapter - Java Instant precision is 9 while PostgreSQL timestamp precision is 6 (rounded). As the result, JaVers reports created Instant as updated for an update after an insert (which JaVers records with nanos).

  • repository.save(entity) is called for a new entity
    • new Instant value with nanos is generated by @org.springframework.data.annotation.CreatedDate annotation processor
    • JaVers intercepts the entity.created value passed to repository.save(entity)
    • actual value stored in the PostgreSQL is set to rounded micros
  • repository.findById(id) is called to retrieve an entity
    • entity is returned with entity.created set to rounded micros (as retrieved from PostgreSQL)
  • an entity property is modified (e.g. name) and repository.save(entity) is called for an updated entity
  • JaVers intercepts the entity.created value passed repository.save(entity)
    • JaVers reports the entity.created value changed because the insert save call contained nanos and a subsequent update save call contained rounded micros retrieved from PostgreSQL

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment