Skip to content
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

How are you guys handling dates in Spring Boot? #523

Closed
cwendel16834 opened this issue Dec 20, 2019 · 8 comments
Closed

How are you guys handling dates in Spring Boot? #523

cwendel16834 opened this issue Dec 20, 2019 · 8 comments

Comments

@cwendel16834
Copy link

I am trying to add a custom scalar type to deserialize the string representation of a date ("2020-01-30") into a java.time.LocalDate. I get a failure that Jackson cannot handle this without registering the JavaTimeModule on the ObjectMapper. I can seem to get past this particular error by declaring a @bean for an object mapper, but then other things fail. I believe this is because I have replaced the implicit ObjectMapper. Any suggestions?

@dariuszkuc
Copy link
Collaborator

Creating custom object mapper bean should be fine, unsure what sort of errors you are hitting. In general if you are using some autoconfiguration libraries it might be safer to use jackson customizer instead (Jackson2ObjectMapperBuilderCustomizer) -> see https://docs.spring.io/spring-boot/docs/current/reference/html/howto.html#howto-customize-the-jackson-objectmapper

@cwendel16834
Copy link
Author

After further research, it seems that the generator instantiates its own ObjectMapper via a call to jacksonObjectMapper() in KotlinDataFetcherFactoryProvider.kt

I was able to extend this class as shown below in order to introduce an ObjectMapper with the JavaTimeModule registered.

// Overrides the default provider in order to register the JavaTimeModule on the ObjectMapper used by the schema generator.
// This allows the Jackson Object Mapper to properly handle time types including java.time.LocalDate
@Component
class KotlinDataFetcherFactoryProviderWithJavaTimeModule(private val hooks: SchemaGeneratorHooks) : KotlinDataFetcherFactoryProvider(hooks) {
    override fun functionDataFetcherFactory(
            target: Any?,
            kFunction: KFunction<*>): DataFetcherFactory<Any> {
        return DataFetcherFactories.useDataFetcher(
                FunctionDataFetcher(
                        target = target,
                        fn = kFunction,
                        objectMapper = jacksonObjectMapper().registerModule(JavaTimeModule()),
                        executionPredicate = hooks.dataFetcherExecutionPredicate))
    }
}

Here's the original:

open class KotlinDataFetcherFactoryProvider(private val hooks: SchemaGeneratorHooks) {

    private val defaultObjectMapper = jacksonObjectMapper()

    open fun functionDataFetcherFactory(target: Any?, kFunction: KFunction<*>): DataFetcherFactory<Any> =
            DataFetcherFactories.useDataFetcher(
                FunctionDataFetcher(
                        target = target,
                        fn = kFunction,
                        objectMapper = defaultObjectMapper,
                        executionPredicate = hooks.dataFetcherExecutionPredicate))

    ...
}

Since the object mapper is private and instantiated directly, I am unable to modify the existing one and must mimic the flow. Same with the hooks passed in. It feels a bit fragile, but works for LocalDate per my testing.

@cwendel16834
Copy link
Author

Is there any reason the JavaTimeModule couldn't be regisered by default or by configuration? How have you guys solved this before? Surely you are using dates in your Schema.

@dariuszkuc
Copy link
Collaborator

dariuszkuc commented Dec 20, 2019

While using String for date representation could definitely work it has some drawbacks as it is not really strongly typed. Since it is a custom scalar your client have no idea what is the expected value besides the fact that it is serialized as a String. It also leads to problems on what is the expected pattern -> is it year-month-day? day-month-year? or maybe its using month names instead of numbers?

IMHO it is much cleaner to expose this sort of data explicitly, e.g. something like

data class ZonedDateTime(
    val day: Int,
    val month: Int,
    val year: Int,
    val hour: Int,
    val minute: Int,
    val second: Int,
    val timeZoneOffsetSeconds: Int
) 

You could take it even further to use some enum for months or easily perform some extra validations.

@dariuszkuc
Copy link
Collaborator

Thanks for bringing it up. I think spring-server logic should use object mapper from spring context - created #524 issue to track it.

@cwendel16834
Copy link
Author

RE: Serialization format:

I agree that using the String type to serialize isn't great. One step up seems to be a custom scalar that maps to an ISO_LOCAL_DATE format string. For my uses, I intend to generate Typescript types from my GraphQL schema, at which point my LocalDate scalar will be mapped into a MomentJS object by parsing the ISO string. I think I prefer this becase the ISO standard is already established and I will just have to define that conversion once when my types are generated. All consumers will then work directly with the MomentJS api and know what to expect. My current endeavor is a solo project, so I have the luxury of deciding this format for the product unilaterily.

RE: Spring ObjectMapper:
Thanks for creating the issue to track it. I was surprised when overriding the ObjectMapper didn't seem to make a difference.

@dariuszkuc
Copy link
Collaborator

Take a look at graphql/graphql-spec#635 it somewhat addresses the ambiguity of custom scalar.

@dariuszkuc
Copy link
Collaborator

Closing this issue as it was superseded by #524 (resolved in #525 )

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

No branches or pull requests

2 participants