Skip to content

Allow the formatting of java.time.LocalDate etc to be configured via the environment #4217

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
sixrandanes opened this issue Oct 16, 2015 · 20 comments
Labels
status: duplicate A duplicate of another issue type: enhancement A general enhancement

Comments

@sixrandanes
Copy link

sixrandanes commented Oct 16, 2015

Hello,

1/ Description

I try to serialize a LocalDate (java 8) from my JPA entity into a specific pattern (dd/MM/yyyy).
Everything works fine if I put the annotation @JsonFormat(pattern="dd/MM/yyyy") on each date of my JPA entity.
That solution is too heavy because I don't want to put this annotation on each date of all my jpa entites, So, I was looking for a better solution by applying some global configuration with spring boot.

I found the properties spring.jackson.date-format and spring.jackson.locale but I can't get them to work. I don't know if it's really a bug or I miss something in my project / configuration.

I am using Java8, spring-boot (1.2.6.RELEASE) with spring data jpa 1.9, and I have this in my classpath : 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.6.3'

To be precise that I am only working with spring-data-jpa and not spring-data-rest.

2/ Unit testing

I made a unit test and I have that output :
(I use spring.jackson.date-format property)

Without the property in my yaml file, I've got this :

java.lang.AssertionError: JSON path$[0].dateCreation
Expected: "16/10/2015"
     but: was **<[2015,10,16]>**

With the property but without specifying any format (blank), I've got this :

java.lang.AssertionError: JSON path$[0].dateCreation
Expected: "16/10/2015"
     but: was **"2015-10-16"**

With the property and If i specify any format (dd/MM/yyyy or dd-mm-yyyy, or even a class) , I've got the same:

java.lang.AssertionError: JSON path$[0].dateCreation
Expected: "16/10/2015"
     but: was **"2015-10-16"**

I've got the same result with the property : spring.jackson.locale

3/ build.gradle

    apply plugin: 'spring-boot'
    apply plugin: 'java'

    springBoot {
        mainClass = "com.seriajack.Application"
    }

    repositories {
        mavenCentral()
    }

    group = 'test'
    version = "1.0"

    jar {
        baseName = 'seriajack'
        version= '1.0'
    }

    compileJava.options.encoding = 'UTF-8'
    sourceCompatibility = 1.8
    targetCompatibility = 1.8

buildscript {
    repositories {
                mavenCentral()
        }

    dependencies {
        classpath(group: 'org.springframework.boot', name: 'spring-boot-gradle-plugin', version: '1.2.6.RELEASE')
    }
}

    dependencies {
        compile("com.google.guava:guava:18.0")
        compile("org.jolokia:jolokia-core:1.2.2")
        compile('com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.6.3')
        compile("org.springframework.boot:spring-boot-starter-web")
        compile("org.springframework.boot:spring-boot-starter-tomcat")
        compile("org.springframework.boot:spring-boot-starter-data-jpa")
        compile("org.springframework.data:spring-data-jpa:1.9.0.RELEASE")
        compile("com.h2database:h2")
        compile("org.flywaydb:flyway-core:3.2.1")
        testCompile("org.springframework.boot:spring-boot-starter-test")        
        testCompile 'junit:junit:4.12'
        testCompile 'org.easytesting:fest-assert:1.4'
        testCompile 'org.mockito:mockito-all:1.10.19'
        testCompile 'com.jayway.jsonpath:json-path:2.0.0'
    }

4/ application.yml

spring:
 profiles:
  active: test
 jpa:
  show-sql: false
  hibernate:
   ddl: auto  
 application:
  name: seriajack
 boot:
  admin:
   url: http://localhost:8080
   client:
    serviceUrl: http://localhost:8080
 jackson: 
  date-format: dd/MM/yyyy

management:
 port: 8080
 address: 127.0.0.1

#SERVER
server:
 port: 8080
 session-timeout: 300
 tomcat.uri-encoding: UTF-8

---
#PROFILE LOCAL
spring:
 profiles: local
 datasource:
  initialize: false
datasource:
 seriajack:
  driverClassName: oracle.jdbc.driver.OracleDriver
  url: 
  username:
  password:
--- 
#PROFILE DEV
spring:
 profiles: dev
 datasource:
  initialize: false
datasource:
 seriajack:
  driverClassName: oracle.jdbc.driver.OracleDriver
  url: 
  username:
  password:
---  
#PROFILE TEST
spring:
 profiles: test
 datasource:
  initialize: false
  platform: h2
datasource:
 test:
  url: "jdbc:h2:mem:test;USER=SA;MODE=Oracle;DB_CLOSE_ON_EXIT=TRUE;INIT=create schema if not exists SERIAJACK\\;"
  driverClassName: org.h2.Driver

5/ application.java

@SpringBootApplication
@EntityScan(basePackageClasses = { Application.class, Jsr310JpaConverters.class})
public class Application {

    public static void main(String[] args) throws Exception {
        SpringApplication.run(Application.class, args);
    }
}

6/ JPA entity

package com.seriajack.textereglementaire;

import java.time.LocalDate;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

import com.fasterxml.jackson.annotation.JsonFormat;

@Entity
@Table(name = "T_TEXTE_REGLEMENTAIRE", schema = "SERIAJACK")
public class TexteReglementaire {

    private long id;
    private TexteType type;
    private String numero;
    private String intitule;
    private LocalDate dateCreation;
    @Lob
    private byte[] fichier;
    private String precision;
    private LocalDate dateCertification;
    private LocalDate dateParution;
    private String numeroJONC;
    private LocalDate dateAbrogation;
    private String reference;
    private String lienJuridoc;

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "SEQ_TTEXTEREG")
    @SequenceGenerator(name = "SEQ_TTEXTEREG", sequenceName = "SERIAJACK.SEQ_TTEXTEREG")
    @Column(name = "ID")
    public long getId() {
        return id;
    }

    public void setId(long id) {
        this.id = id;
    }

    @Column(name = "TYPE", nullable = false)
    @Enumerated(EnumType.STRING)
    public TexteType getType() {
        return type;
    }

    public void setType(TexteType type) {
        this.type = type;
    }

    @Column(name = "NUMERO", nullable = false)
    public String getNumero() {
        return numero;
    }

    public void setNumero(String numero) {
        this.numero = numero;
    }

    @Column(name = "INTITULE", nullable = false)
    public String getIntitule() {
        return intitule;
    }

    public void setIntitule(String intitule) {
        this.intitule = intitule;
    }

    @Column(name = "DATE_CREATION", nullable = false)
    public LocalDate getDateCreation() {
        return dateCreation;
    }

    public void setDateCreation(LocalDate dateCreation) {
        this.dateCreation = dateCreation;
    }

    @Column(name = "FICHIER", nullable = false)
    public byte[] getFichier() {
        return fichier;
    }

    public void setFichier(byte[] fichier) {
        this.fichier = fichier;
    }

    @Column(name = "PRECISION")
    public String getPrecision() {
        return precision;
    }

    public void setPrecision(String precision) {
        this.precision = precision;
    }

    @Column(name = "DATE_CERTIFICATION")
    public LocalDate getDateCertification() {
        return dateCertification;
    }

    public void setDateCertification(LocalDate dateCertification) {
        this.dateCertification = dateCertification;
    }

    @Column(name = "DATE_PARUTION")
    public LocalDate getDateParution() {
        return dateParution;
    }

    public void setDateParution(LocalDate dateParution) {
        this.dateParution = dateParution;
    }

    @Column(name = "NUMERO_JONC")
    public String getNumeroJONC() {
        return numeroJONC;
    }

    public void setNumeroJONC(String numeroJONC) {
        this.numeroJONC = numeroJONC;
    }

    @Column(name = "DATE_ABROGATION")
    public LocalDate getDateAbrogation() {
        return dateAbrogation;
    }

    public void setDateAbrogation(LocalDate dateAbrogation) {
        this.dateAbrogation = dateAbrogation;
    }

    @Column(name = "REFERENCE")
    public String getReference() {
        return reference;
    }

    public void setReference(String reference) {
        this.reference = reference;
    }

    @Column(name = "LIEN_JURIDOC")
    public String getLienJuridoc() {
        return lienJuridoc;
    }

    public void setLienJuridoc(String lienJuridoc) {
        this.lienJuridoc = lienJuridoc;
    }
}

7/ Controller

@Configuration
@RestController
@RequestMapping(value = "/api/texte", produces = "application/json;charset=UTF-8")
public class TexteReglementaireController {

    @Autowired
    TexteReglementaireService texteReglementaireService;

    @RequestMapping(value = "/list", method = RequestMethod.GET)
    public List<TexteReglementaire> findAllTexteReglementaire() {
        return texteReglementaireService.findAllTexteReglementaire();
    }
}
@sixrandanes
Copy link
Author

sixrandanes commented Oct 20, 2015

Seem like the bean Jackson2ObjectMapperBuilder is not configured with my properties in yaml, because I have already a Jackson2ObjectMapperBuilder from spring-web...

@Bean
@ConditionalOnMissingBean(Jackson2ObjectMapperBuilder.class)
public Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder() {

Am I right ?

@wilkinsona
Copy link
Member

That code isn't from spring-web. @ConditionalOnMissingBean is only used in Spring Boot. Are you referring to JacksonAutoConfiguration.JacksonObjectMapperBuilderConfiguration? If so, then that builder is configured using your properties and then used to create the context's ObjectMapper.

Rather than copy and pasting snippets of code into an issue, can you please provide a project that we can git clone or a zip that we can download and build that reproduces the problem?

@wilkinsona wilkinsona added the status: waiting-for-feedback We need additional information before we can continue label Oct 20, 2015
@sixrandanes
Copy link
Author

sixrandanes commented Oct 20, 2015

Here is the link where you can find my project :

https://github.com/sixrandanes/seriajack

There is a failure test which describes the problem : TexteReglementaireControllerTestIT

Thank you in advance for your reply

@wilkinsona wilkinsona removed the status: waiting-for-feedback We need additional information before we can continue label Oct 20, 2015
@wilkinsona wilkinsona self-assigned this Oct 22, 2015
@wilkinsona
Copy link
Member

spring.jackson.date-format only affects the serialisation of java.util.Date instances. Jackson's JavaTimeModule automatically configures a number of serialisers, but it does so with default formatting which simply calls toString.

If you want to configure the formatting of java.time.* instances then you'll need to manually register the appropriate serialisers configured to use an appropriately configured formatter. For example:

    @Autowired
    private Jackson2ObjectMapperBuilder builder;

    @PostConstruct
    public void postConstruct() {
        this.builder.serializers(new LocalDateSerializer(new DateTimeFormatterBuilder()
                .appendPattern("dd/MM/yyyy").toFormatter()));
    }

@wilkinsona wilkinsona changed the title Jackson serialization & Java 8 Date : possible bug with property spring.jackson.date-format Allow the formatting of java.time.LocalDate etc to be configured via the environment Oct 22, 2015
@wilkinsona wilkinsona added the type: enhancement A general enhancement label Oct 22, 2015
@wilkinsona wilkinsona removed their assignment Oct 22, 2015
@sixrandanes
Copy link
Author

Thank you very much. It helps me !

@shakuzen
Copy link
Member

+1 For being able to configure the new Date/Time formats via the environment.

@sixrandanes
Copy link
Author

Hello,

I just realized that there is a new property :

spring.jackson.joda-date-time-format= 

Is that property have something to do with that issue ?

@snicoll
Copy link
Member

snicoll commented Nov 25, 2015

No, that's the joda date format, not the Java8 date format.

@sixrandanes
Copy link
Author

ok sorry, I red too fast :-)

@cbornet
Copy link

cbornet commented Jul 13, 2016

@wilkinsona I tried the @PostConstruct workaround but couldn't make it work. Is there anything else to do than putting it in a @Configuration class ?

@wilkinsona
Copy link
Member

@cbornet I don't believe so. If you're having trouble please post on Stack Overflow using the spring-boot tag with a minimal example that reproduces the problem.

@cbornet
Copy link

cbornet commented Jul 13, 2016

I had to add

    @Bean
    public ObjectMapper myJacksonObjectMapper() {
        return this.builder.createXmlMapper(false).build();
    }

to make it work. If not present, the autoconfigured objectMapper is created before the Jackson2ObjectMapperBuilder customization.

Also, I see that in SB 1.4, there will be a Jackson2ObjectMapperBuilderCustomizer that will make all this cleaner.

@cbornet
Copy link

cbornet commented Sep 14, 2016

For reference, Jackson2ObjectMapperBuilderCustomizer works like a charm for this.
Having configuration file properties would be even nicer though !

@xyalan
Copy link

xyalan commented Dec 7, 2016

Work for me now.

@drenda
Copy link

drenda commented Jul 2, 2017

+1 to this.
I'm using the workaround suggested here. Can it manage also the timezone?
I want avoid to put this annotation on my bean's fields:

@JsonFormat(timezone = "UTC", pattern = "yyyy-MM-dd'T'HH:mm:ss.SSSZ")

I set:

@PostConstruct
	public void postConstruct() {
                this.builder.timeZone("UTC");
		this.builder.serializers(
				new LocalDateTimeSerializer(new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSS").toFormatter()));
		this.builder.serializers(
				new ZonedDateTimeSerializer(new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").toFormatter()));
}

but In REST reply I still see the time in my local timezone.

@cbornet
Copy link

cbornet commented Jul 2, 2017

@drenda you should use a Jackson2ObjectMapperBuilderCustomizer bean instead

@drenda
Copy link

drenda commented Jul 2, 2017

@cbornet Should this solve my timezone problem?

@Bean
	public Jackson2ObjectMapperBuilderCustomizer jackson2ObjectMapperBuilderCustomizer() {
		return new Jackson2ObjectMapperBuilderCustomizer() {

			@Override
			public void customize(Jackson2ObjectMapperBuilder jacksonObjectMapperBuilder) {
				jacksonObjectMapperBuilder.timeZone("UTC");
				jacksonObjectMapperBuilder.serializers(
						new LocalDateTimeSerializer(new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd'T'HH:mm:ss").toFormatter()));
				jacksonObjectMapperBuilder.serializers(new ZonedDateTimeSerializer(
						new DateTimeFormatterBuilder().appendPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ").toFormatter()));
			}

		};
	}

In my case doesn't work. The timezone seems to be my local timezone instead of UTC. Thanks

@cbornet
Copy link

cbornet commented Jul 3, 2017

@drenda for which date type ? .timezone("UTC") only configures java.util.Date and not the java.time.* types. It seems to be a configuration issue and not a bug so can you post your issue on StackOverFlow ?

@drenda
Copy link

drenda commented Jul 3, 2017

@cbornet I'm using LocalDateTime (jdk8). I figured .timezone() works only with java.util.Date. I guessed the workaraound suggested here could work also to set up the timezone like done for the serialization how I shown up. I'll post on StackOverFlow. Thanks

@bclozel
Copy link
Member

bclozel commented Mar 23, 2018

Duplicates #5523

@bclozel bclozel closed this as completed Mar 23, 2018
@bclozel bclozel added the status: duplicate A duplicate of another issue label Mar 23, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: duplicate A duplicate of another issue type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

9 participants