Type Conversion Between Java and TypeScript
TypeScript to 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 are 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 opposite direction.
The default Hilla JSON ObjectMapper
closely follows the Spring Boot auto-configuration defaults.
One notable difference is that, in Hilla, the default object mapper is configured to discover private
properties.
Hence, all the fields, getters, setters and constructors are discoverable, even if they are declared as private
.
This is done in order to make serializing / deserializing custom objects easier.
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.
This 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 will return 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
.
The default conversion rules are summarized as follows (TypeScript-compliant values are converted to the corresponding values, otherwise the backend returns an error message):
To Receive Primitive Types in Java
Type boolean
:
-
TypeScript-compliant values:
-
A boolean value:
true
⇒true
andfalse
⇒false
-
-
Non-compliant values:
-
Any value that’s not a valid
boolean
type in TypeScript
-
Type char
:
-
TypeScript-compliant values:
-
A single character string:
'a'
⇒'a'
-
-
Non-compliant values:
-
Any string value that has more than one character
-
Any value that’s not a valid
string
type in TypeScript
-
-
UTF-16 and Unicode: Both Java and TypeScript internally use UTF-16 for string encoding. This makes string conversion between backend and frontend trivial. However, using UTF-16 has its limitations and corner cases. Most notably, a string like
"🥑"
might seem like a single-character that can be passed to Java as achar
. However, both in TypeScript and Java, it’s actually a two-character string, because theU+1F951
symbol takes two characters in UTF-16:\uD83E\uDD51
. Thus, it’s not a valid value for the Javachar
type.
Type byte
:
-
TypeScript-compliant values:
-
An integer or decimal number in the range
-129 < X < 256
.100
,100.0
and100.9
⇒100
-
-
Non-compliant values:
-
Any value which isn’t a number in TypeScript
-
Any number value which is outside the compliant range
-
-
Overflow number: if TypeScript sends a value which is greater than Java’s
Byte.MAX_VALUE
(28 - 1), the bits get rolled over. For example, if you send a value of128
(Byte.MAX_VALUE + 1
), the Java side receives-128
(Byte.MIN_VALUE
). -
Underflow number: if the Java side expects a
byte
value but TypeScript sends an underflow number, for example-129
(Byte.MIN_VALUE - 1
), the backend returns an error.
Type short
:
-
TypeScript-compliant values:
-
An integer or decimal number in the range
-216 < X < 216 - 1
.100
,100.0
and100.9
⇒100
-
-
Non-compliant values:
-
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
short
.
Type int
:
-
TypeScript-compliant values:
-
An integer or decimal number:
100
,100.0
and100.9
⇒100
-
-
Non-compliant values:
-
Any value which isn’t a number in TypeScript
-
-
Overflow number: if TypeScript sends a value which is greater than Java’s
Integer.MAX_VALUE
(231 - 1), the bits get rolled over. For example, if you send a value of231
(Integer.MAX_VALUE + 1
), the Java side receives-231
(Integer.MIN_VALUE
). -
Underflow number: the situation is reversed with overflow numbers. If you send
-231 - 1
(Integer.MIN_VALUE - 1
), the Java side receives231 - 1
(Integer.MAX_VALUE
).
Type long
:
-
TypeScript-compliant values:
-
An integer or decimal number:
100
,100.0
and100.9
⇒100
-
-
Non-compliant values:
-
Any value which isn’t a number in TypeScript
-
-
Overflow and underflow numbers: bits get rolled over when receiving overflow/underflow numbers; that is,
263
⇒-263
,-263 - 1
⇒263 - 1
Type float
and double
:
-
TypeScript-compliant values:
-
An integer or decimal number:
100
and100.0
⇒100.0
,100.9
⇒100.9
-
-
Non-compliant values:
-
Any value which isn’t a number in TypeScript
-
-
Overflow and underflow numbers are converted to
Infinity
and-Infinity
respectively.
To Receive a String in Java
String
values are kept the same when sent from TypeScript to the Java backend.
To Receive Date Time Types in Java
java.util.Date
-
TypeScript-compliant values:
-
A string that represents an epoch timestamp in milliseconds:
'1546300800000'
is converted to ajava.util.Date
instance that contains the value of the date2019-01-01T00:00:00.000+0000
.
-
-
Non-compliant values:
-
A non-number string, for example
'foo'
-
java.time.Instant
-
TypeScript-compliant values:
-
A string that represents an epoch timestamp in seconds:
'1546300800'
is converted to ajava.time.Instant
instance that contains the value of2019-01-01T00:00:00Z
.
-
-
Non-compliant values:
-
A non-number string, for example
'foo'
-
java.time.LocalDate
-
TypeScript-compliant values:
-
A string that follows the
java.time.format.DateTimeFormatter#ISO_LOCAL_DATE
formatyyyy-MM-dd
:'2018-12-16'
,'2019-01-01'
.
-
-
Non-compliant values:
-
An incorrect-format string, for example
'foo'
-
java.time.LocalDateTime
-
TypeScript-compliant values:
-
A string that follows the
java.time.format.DateTimeFormatter#ISO_LOCAL_DATE_TIME
format:-
With full time:
'2019-01-01T12:34:56'
-
Without seconds:
'2019-01-01T12:34'
-
With full time and milliseconds:
'2019-01-01T12:34:56.78'
-
-
-
Non-compliant values:
-
An incorrect-format string, for example
'foo'
-
To Receive an Enum in Java
-
TypeScript-compliant value:
-
A string with the same name as an enum. Assume that we have an [enum-declaration]; then sending
"FIRST"
from TypeScript would result in an instance ofFIRST
withvalue=1
in Java.
-
public enum TestEnum {
FIRST(1), SECOND(2), THIRD(3);
private final int value;
TestEnum(int value) {
this.value = value;
}
public int getValue() {
return this.value;
}
}
-
Non-compliant values:
-
A non-matched string with name of the expected Enum type
-
Any other types: boolean, object or array
-
To Receive an Array in Java
-
TypeScript-compliant values:
-
An array of items with expected type in Java, for example:
-
Expected in Java
int[]
:[1, 2, 3]
⇒[1,2,3]
,[1.9, 2, 3]
⇒[1,2,3]
-
Expected in Java
String[]
:["foo","bar"]
⇒["foo","bar"]
-
Expected in Java
Object[]
:["foo", 1, null, "bar"]
⇒["foo", 1, null, "bar"]
-
-
-
Non-compliant values:
-
A non-array input:
"foo"
,"[1,2,3]"
,1
-
To Receive a Collection in Java
-
TypeScript-compliant values:
-
An array of items with expected type in Java (or types that can be converted to expected types). For example, if you expected in Java:
-
Collection<Integer>
:[1, 2, 3]
⇒[1,2,3]
-
Collection<String>
:["foo","bar"]
⇒["foo","bar"]
-
Set<Integer>
:[1, 2, 2, 3, 3, 3]
⇒[1, 2, 3]
-
-
-
Non-compliant values:
-
A non-array input:
"foo"
,"[1,2,3]"
,1
-
To Receive a Map in Java
-
TypeScript-compliant value:
-
A TypeScript object with
string
key and value of the expected type in Java. For example, if the expected type in Java isMap<String, Integer>
, the compliant object in TypeScript should have a type of{ [key: string]: number; }
, for example{one: 1, two: 2}
.
-
-
Non-compliant values:
-
A value of another type
-
Note
|
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.
|
To Receive a Bean in Java
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.
-
Example: assuming that we have [bean-example], a valid input for the bean looks like:
{
"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;
}
}
Java to TypeScript
The same object mapper used when converting from TypeScript to 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.
Type "number"
All the Java types that extend java.lang.Number
are deserialized to number
in TypeScript.
There are a few exceptional cases with extremely large or 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 correctly compared.
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.
Type "string"
The primitive type char
, its boxed type Character
and String
in Java are converted to string
type in TypeScript.
Type "boolean"
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 the 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 your endpoint method returns a MyBean
type, when you call the method, you will 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; }
.
Date Time
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'