Type Conversion between Java & TypeScript
When converting data types from Java to TypeScript and vice versa, you should understand certain particulars so you won’t have problems. They’re explained on this page.
Receiving TypeScript Values in Java
When calling a Java endpoint method from TypeScript, ConnectClient
serializes TypeScript call parameters to JSON and sends them to the Java backend. There they’re deserialized into Java types using the Jackson JSON processing library. The return value of the Java endpoint method is sent back to TypeScript through the same pipeline in the other direction.
JSON Object Mapper
The default Hilla JSON ObjectMapper
follows the Spring Boot auto-configuration defaults.
The visibility level of the default ObjectMapper
can be configured by setting the spring.jackson.visibility
property in common application properties.
Other properties of the default ObjectMapper
can be customized by following the Spring Boot documentation on the subject. Alternatively, the entire ObjectMapper
can be replaced with a custom one by providing an ObjectMapper
bean with the qualifier defined in com.vaadin.connect.HillaConnectController#VAADIN_ENDPOINT_MAPPER_BEAN_QUALIFIER
.
The default ObjectMapper
always converts TypeScript values to a JSON object before sending them to the backend, so that the values need to be compliant with the JSON specification. It only accepts values from the following types: string
, number
, array
, boolean
, JSON object or null
. This implies that NaN
and Infinity
are non-compliant. If these values are sent, the server returns an error response: 400 Bad Request
. Sending a parameter of undefined
from TypeScript results in default values for primitive types, null
for a Java object, and Optional.empty()
for Optional
.
Default Conversion Rules
The default conversion rules are summarized in the following table. Incidentally, TypeScript-compliant values are converted to the corresponding values, otherwise the backend returns an error message.
Primitive Types
Type | Compliant Values | Non-Compliant Values | Overflow/Underflow Values |
---|---|---|---|
| A boolean value: | Any value that’s not a valid | N/A |
| A single character string: | Any string value that has more than one character. Any value that’s not a valid | N/A |
| An integer or decimal number in the range | Any value which isn’t a number in TypeScript. Any number value which is outside the compliant range. | If TypeScript sends a value which is greater than Java’s |
| An integer or decimal number in the range | Any value which isn’t a number in TypeScript. Any number value which is outside the compliant range. | Overflow and underflow numbers aren’t accepted for |
| An integer or decimal number: | Any value which isn’t a number in TypeScript. | If TypeScript sends a value which is greater than Java’s |
| An integer or decimal number: | Any value which isn’t a number in TypeScript. | Bits are rolled over when receiving overflow/underflow numbers; that is, |
| An integer or decimal number: | Any value which isn’t a number in TypeScript. | Overflow and underflow numbers are converted to |
Date-time types
Type | Compliant values | Non-compliant values |
---|---|---|
| A string that represents a timestamp in epoch, ISO-8601, or RFC-1123 format is converted to a That is to say, an epoch timestamp in milliseconds (e.g., | A non-numeric string that doesn’t conform to ISO-8601 or to RFC-1123 format (e.g., |
| A string that represents an epoch timestamp in seconds. For example, Also, a string that follows the | A non-numeric string that doesn’t conform to |
| A string that follows the | An incorrect-format string (e.g., |
| A string that follows the | An incorrect-format string (e.g., |
Enum
A compliant TypeScript value is a string which equals an enum name in Java. The Java enum
type is mapped to an enum
TypeScript type. It’s an object type, so you can work with it as you work with regular TypeScript objects.
enum
type in Java-
public enum Enumeration { FIRST, SECOND, }
- Generated
enum
type in TypeScript -
export enum Enumeration { FIRST = "FIRST", SECOND = "SECOND" }
Note
|
Complex Java enum Mapping
The enum type is mapped in a simple way. No constructor-related Java features are available in the TypeScript enum .
|
- Complex
enum
type in Java -
public enum Enumeration { FIRST("ONE"), SECOND("TWO"); private String value; public Enumeration(String value) { this.value = value; } public String getValue() { return value; } }
- Generated complex
enum
type in TypeScript -
export enum Enumeration { FIRST = "FIRST", SECOND = "SECOND" }
Non-compliant values include a non-matched string with name of the expected enum type, and any other types: boolean
, number
, object
, or array
.
Array
Compliant TypeScript values are arrays of items with the same type as is expected in Java. For example:
Expected type in Java | TypeScript value | Converted value in Java |
---|---|---|
|
|
|
|
|
|
|
|
|
Values of any other type are non-compliant (e.g., true
, "foo"
, "[1,2,3]"
, or 1
).
Collection
Compliant TypeScript values are arrays of items with the same type as expected in Java, or types that can be converted to the expected type. For example:
Expected Type in Java | TypeScript Value | Converted Value in Java |
---|---|---|
|
|
|
|
|
|
|
|
|
Values of any other type are non-compliant, for example: true
, "foo"
, "[1,2,3]"
, or 1
.
Map
Compliant values are TypeScript objects with a string
key and a value of the expected type in Java. For example, if the expected type in Java is Map<String, Integer>
, the compliant object in TypeScript should have a type of { [key: string]: number; }
(e.g., {one: 1, two: 2}
). Values of any other type are non-compliant.
Due to the fact that the TypeScript code is generated from the OpenAPI TypeScript Endpoints Generator and the OpenAPI specification has a limitation for the map type, the map key is always a string
in TypeScript.
Bean
A bean is parsed from the input JSON object, which maps the keys of the JSON object to the property name of the bean object. You can also use Jackson’s annotation to customize your bean object. For more information about the annotations, see Jackson Annotations.
As an example, assume that you have Bean example. A valid input for the bean would look like this:
{
"name": "MyBean",
"address": "MyAddress",
"age": 10,
"isAdmin": true,
"customProperty": "customValue"
}
public class MyBean {
public String name;
public String address;
public int age;
public boolean isAdmin;
private String customProperty;
@JsonGetter("customProperty")
public String getCustomProperty() {
return customProperty;
}
@JsonSetter("customProperty")
public void setCustomProperty(String customProperty) {
this.customProperty = customProperty;
}
}
JsonNode
The JsonNode
Jackson type can be used to receive any kind of JSON object in Java. The JsonNode
type is a tree model for JSON objects, and it can be used to navigate through the JSON object. The JsonNode
type is converted to a TypeScript object with the type unknown
. This means that the client-side code must correctly handle the type.
Receiving Java Types in TypeScript
The same object mapper used when converting from Receiving TypeScript Values in Java deserializes the return values in Java to the corresponding JSON object before sending them to the client side.
Type Conversion can be customized by using annotations on the object to serialize, as described in Customizing Type Conversion.
Number Type
All Java types that extend java.lang.Number
are deserialized to number
in TypeScript. There are a few exceptions with extremely large or extremely small numbers. The safe integer range is from -(253 − 1)
to 253 − 1
. This means that only numbers in this range can be represented exactly and compared correctly. See more information about safe integers.
In fact, not all long
numbers in Java can be converted correctly to TypeScript, since its range is -263
to 263 − 1
. Unsafe numbers are rounded using the rules defined in the IEEE-754 standard.
Special values such as NaN
, POSITIVE_INFINITY
and NEGATIVE_INFINITY
are converted into string
when sent to TypeScript.
String Type
The primitive type char
, its boxed type Character
and String
in Java are converted to string
type in TypeScript.
Boolean Type
The boolean
and Boolean
in Java are converted to boolean
type when received in TypeScript.
Array of Items
Normal array types such as int[]
, MyBean[]
and all types that implement or extend java.lang.Collection
become array
when they are sent to TypeScript.
Object
Any kind of object in Java is converted to the corresponding defined type in TypeScript. For example, if the endpoint method returns a MyBean
type, when you call the method, you’ll receive an object of type MyBean
. If the generator cannot get information about your bean, it returns an object of type any
.
Map
All types that inherit from java.lang.Map
become objects in TypeScript with string
keys and values of the corresponding type. For instance: Map<String, Integer>
⇒ { [key: string]: number; }
.
Datetime
By default, the ObjectMapper
converts Java’s date time to a string in TypeScript, with the following formats:
-
java.util.Date
of00:00:00 January 1st, 2019
⇒'2019-01-01T00:00:00.000+0000'
-
java.time.Instant
of00:00:00 January 1st, 2019
⇒'2019-01-01T00:00:00Z'
-
java.time.LocalDate
of00:00:00 January 1st, 2019
⇒'2019-01-01'
-
java.time.LocalDateTime
of00:00:00 January 1st, 2019
⇒'2019-01-01T00:00:00'
Custom Type Conversions
When serializing and deserializing data in Java endpoints, you might be interested in renaming properties and excluding certain properties and types.
Omitting properties helps the application avoid sending sensitive data, such as password fields. Leaving out types helps to simplify the TypeScript-exported classes, and to avoid circular dependencies in the serialized JSON output.
Hilla relies on the Jackson JSON library to do serialization, so it’s possible to use their annotations to rename properties or exclude data.
The @JsonProperty
Annotation
The @JsonProperty
annotation is used to define a method as a setter or getter for a logical property, or to define a field to be serialized and deserialized as a specific logical property.
The annotation value indicates the name of the property in the JSON object. By default, it takes the Java name of the method or field.
public class Student {
@JsonProperty("bookId")
private String id;
private String name;
@JsonProperty("name")
public void setFirstName(String name) {
this.name = name;
}
@JsonProperty("name")
public String getFirstName() {
return name;
}
@JsonProperty
public int getRating() {
return StudentRating.getRatingFor(name);
}
}
The @JsonIgnore
Annotation
The @JsonIgnore
annotation indicates that the logical property used in serializing and deserializing for the accessor (i.e., field, getter or setter) is to be ignored.
@JsonIgnore
private String category;
@JsonIgnore
public String getCategory() {
return category;
}
@JsonIgnore
public void setCategory(String category) {
this.category = category;
}
@JsonIgnoreProperties
Annotation
The @JsonIgnoreProperties
annotation ignores a set of logical properties in serializing and deserializing. It must be used at class level.
@JsonIgnoreProperties(value = { "id"}, allowGetters = true)
public class Product {
private String id;
private String name;
...
}
In addition to the properties passed as the annotation value, the @JsonIgnoreProperties
annotation accepts the following options:
allowSetters
For ignored properties, allowSetters
allows you to set properties when deserializing, but doesn’t list them in serialization.
In the following snippet, password
would not be in the payload returned to TypeScript, but TypeScript can set it:
@JsonIgnoreProperties(value = { "password"}, allowSetters = true)
public class User {
private String name;
private String password;
...
}
allowGetters
For ignored properties, allowGetters
lists them in the serialized object, but doesn’t allow you to set it.
This is useful for read-only properties:
@JsonIgnoreProperties(value = { "id"}, allowGetters = true)
public class Product {
private String id;
private String name;
...
}
ignoreUnknown
During deserializing, ignoreUnknown
prevents an error caused by the presence of a property in the JSON object that has no corresponding property in the Java class.
This is a corner case, and shouldn’t be necessary in Hilla, since the TypeScript-generated API shouldn’t pass unknown properties.
@JsonIgnoreType
Annotation
The @JsonIgnoreType
annotation is a class-level annotation that indicates that all properties of the annotated class type should be ignored during serializing and deserializing.
In the following example, the field client
in Sale
is omitted in the JSON result.
@JsonIgnoreType
public class Client {
...
}
@JsonIgnoreProperties(value = { "password"}, allowSetters = true)
public class Sale {
private Client client;
private Product product;
private int amount;
private double total;
...
}
@JsonCreator
& @JsonValue
Annotations (Domain Objects)
The @JsonCreator
and @JsonValue
annotations are powerful tools in the Jackson JSON library that allow you to create domain objects, also known as single-value objects.
The @JsonCreator
annotation is used to indicate a constructor or factory method that should be used for deserialization. By annotating a constructor or factory method with @JsonCreator
, you can specify how the JSON data should be mapped to the object’s properties.
On the other hand, the @JsonValue
annotation is used to indicate a method that should be used for serialization. By annotating a method with @JsonValue
, you can specify how the object should be converted to a JSON representation.
These annotations provide flexibility and control over the serialization and deserialization process, allowing you to customize how your domain objects are represented in JSON.
When generating the corresponding TypeScript code, Hilla maps the domain object to its raw value type. For example, if the domain object is a single-value object that wraps a string, the TypeScript type is string
.
public class EmailAddress {
private final String value;
@JsonCreator
public EmailAddress(String value) {
this.value = value;
}
@JsonValue
public String getValue() {
return value;
}
}
All usages of EmailAddress
in Java are converted to string
in TypeScript.
"🥑"
might seem like a single-character that can be passed to Java as a char
. However, both in TypeScript and Java, it’s actually a two-character string, because the U+1F951
symbol takes two characters in UTF-16: \uD83E\uDD51
. Thus, it’s not a valid value for the Java char
type.