Docs

Type conversion between Java and TypeScript

Understanding conversion of data types from Java to TypeScript and vice versa.

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 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.

JSON object mapper

The default Hilla JSON ObjectMapper 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 makes 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.

Default conversion rules

The default conversion rules are summarized as follows (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

boolean

A boolean value:
truetrue,
falsefalse.

Any value that’s not a valid boolean type in TypeScript.

N/A

char

A single character string: 'a''a'.[1]

Any string value that has more than one character.
Any value that’s not a valid string type in TypeScript.

N/A

byte

An integer or decimal number in the range -129 < X < 256.

100, 100.0, and 100.9100

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 Byte.MAX_VALUE (28 - 1), the bits get rolled over.

For example, if you send a value of 128 (Byte.MAX_VALUE + 1), the Java side receives -128(Byte.MIN_VALUE).

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.

short

An integer or decimal number in the range -216 < X < 216 - 1. 100, 100.0 and 100.9100.

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.

int

An integer or decimal number: 100, 100.0 and 100.9100.

Any value which isn’t a number in TypeScript.

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 of 231 (Integer.MAX_VALUE + 1), the Java side receives -231 (Integer.MIN_VALUE).

Underflows are the reverse of the overflow case. If you send -231 - 1 (Integer.MIN_VALUE - 1), the Java side receives 231 - 1 (Integer.MAX_VALUE).

long

An integer or decimal number: 100, 100.0 and 100.9100

Any value which isn’t a number in TypeScript.

Bits get rolled over when receiving overflow/underflow numbers; that is, 263-263, -263 - 1263 - 1.

float
double

An integer or decimal number: 100 and 100.0100.0, 100.9100.9.

Any value which isn’t a number in TypeScript.

Overflow and underflow numbers are converted to Infinity and -Infinity respectively.

Boxed primitive types

The conversion works in the same way as primitive types.

String

String values are kept the same when sent from TypeScript to the Java backend.

Date-time types

Type Compliant values Non-compliant values

java.util.Date

A string that represents an epoch timestamp in milliseconds: '1546300800000' is converted to a java.util.Date instance that contains the value of the date 2019-01-01T00:00:00.000+0000.

A non-number string, for example 'foo'.

java.time.Instant

A string that represents an epoch timestamp in seconds: '1546300800' is converted to a java.time.Instant instance that contains the value of 2019-01-01T00:00:00Z.

A non-number string, for example 'foo'.

java.time.LocalDate

A string that follows the java.time.format.DateTimeFormatter#ISO_LOCAL_DATE format yyyy-MM-dd: '2018-12-16', '2019-01-01'.

An incorrect-format string, for example 'foo'.

java.time.LocalDateTime

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'

An incorrect-format string, for example 'foo'.

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 enums 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:

  • A non-matched string with name of the expected enum type.

  • 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

int[]

[1, 2, 3]
[1.9, 2, 3]

[1, 2, 3]
[1, 2, 3]

String[]

["foo", "bar"]

["foo", "bar"]

Object[]

["foo", 1, null, "bar"]

["foo", 1, null, "bar"]

Values of any other type are non-compliant, for example, 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

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]

Values of any other type are non-compliant, for example: true, "foo", "[1,2,3]", or 1.

Map

Compliant values are TypeScript objects with string key and 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; }, for example {one: 1, two: 2}.

Values of any other type are non-compliant.

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.

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.

  • 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;
  }
}

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.

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; }.

Datetime

By default, the ObjectMapper converts Java’s date time to a string in TypeScript, with the following formats:

  • java.util.Date of 00:00:00 January 1st, 2019'2019-01-01T00:00:00.000+0000'

  • java.time.Instant of 00:00:00 January 1st, 2019'2019-01-01T00:00:00Z'

  • java.time.LocalDate of 00:00:00 January 1st, 2019'2019-01-01'

  • java.time.LocalDateTime of 00:00:00 January 1st, 2019'2019-01-01T00:00:00'

null

Returning null from Java throws a validation exception in TypeScript, unless the return type is Optional or the endpoint method is annotated with @Nullable (javax.annotation.Nullable).

Customizing type conversions

When serializing and deserializing data in Java endpoints, the developer 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 (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;
}

The @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.

The @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 will be 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;

    ...
}

1. 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 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.