AutoCrud and UUIDs

Hi everyone,

I am following the guide for Auto CRUD: Auto CRUD component | Vaadin components and have an issue with UUIDs when using a Custom Service as mentioned towards the bottom.

The example uses a record with a Long as an id:

public record ProductDto(Long id, String name, String category, double price) {

I am using UUIDs in the database and hence my record looks like this:

public record ProductDto(UUID id, String name, String category, double price) {

I am able to edit existing entries, but not able to create new ones. When I do this, I get the following error:

com.fasterxml.jackson.databind.JsonMappingException: Invalid UUID string:

I am assuming this is because the id parameter is empty when creating a new entry and an empty String can’t be translated to a UUID.

Does anyone have a pointer in the right direction?

In case you are wondering why I am using a custom service: My JPA entities reference each other and that gives Jackson a problem. If there is a better way to avoid this without DTOs etc, I am also happy to hear about it. But the UUID problem, might pop up somewhere else again, so would be happy to hear how to fix this.

Thank you very much!

Hi,

Could you try making your id nullable? That should lead to use null as id for new entries.

Let me know if it can be a good fix for your use case.

Unfortunately, adding @Nullable didn’t fix the issue for me. In the frontend, I am still getting:

value: Unable to deserialize an endpoint method parameter into type

And in the backend, the same Invalid UUID exception as mentioned above.

I have a test project with

@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private UUID id;

and it is perfectly usable in an AutoCRUD to edit entities and add new ones. I’m not using DTOs in that project.

You should investigate on why there’s an empty string. The only reason I could think of is that the empty string is the default value for a non-nullable string field, that’s why I suggested to make sure it’s nullable.

If you can show how you defined the ID in both the entity and the DTO, maybe that will help us understanding the cause.

Hi Luciano,

First of all a big: Thank you!

I did some debugging and found that the JSON being sent into EndpointInvoker.getVaadinEndpointParameters contained the following:

{
  "id": "",
...

If I change the value of id to null in the debugger to null like this:

{
  "id": null,
...

everything works fine.

As you wrote, it seems like the default value for a field that is not provided is an empty string.

Here is my record:

public record BuildingComponentInfo(
        @Nullable UUID id,
        String name,
        String description,
        BuildingComponentType type,
        PropertyRef property
) {

    public static BuildingComponentInfo fromEntity(BuildingComponent bc) {
        return new BuildingComponentInfo(bc.getId(), bc.getName(), bc.getDescription(), bc.getType(), PropertyRef.fromEntity(bc.getProperty()));
    }

    public record PropertyRef(UUID id, String name) {
        public static PropertyRef fromEntity(Property property) {
            return new PropertyRef(property.getId(), property.getName());
        }
    }
}

And here is the entity:

@Entity
@Table(name = "building_component")
public class BuildingComponent extends AbstractEntity {
    @Column(name = "name", nullable = false)
    private String name;

    @Column(name = "description")
    private String description;

    @Enumerated(EnumType.STRING)
    @Column(name = "type", nullable = false)
    private BuildingComponentType type;

    @ManyToOne
    @JoinColumn(name = "property_id")
    private Property property;

with the superclass

@MappedSuperclass
public abstract class AbstractEntity {

    @Id
    @GeneratedValue
    @UuidGenerator
    private UUID id;

...
}

Here is the config of the AutoCrud component:

<AutoCrud service={BuildingComponentService2} model={BuildingComponentInfoModel}
          gridProps={
              {
                  visibleColumns: ['name', 'description', 'type', 'property'],
                  columnOptions: {
                      property: {
                          renderer: ({item}: { item: BuildingComponentInfo }) =>
                              <span>{item.property.name}</span>,
                      }
                  },
              }}
          formProps={{
              visibleFields: ['name', 'description', 'type', 'property','id'],
              fieldOptions: {
                  id: {
                      readonly: true,
                  },
                  property: {
                      renderer: ({field}) => <ComboBox {...field} items={properties} itemLabelPath='name'/>
                  }
              }
          }}
/>

It seems like, I need to make sure that instead of an empty string, a null value is being passed in from the AutoForm. Do you have any idea how to achieve this? Or is there a better way to solve this.

Thanks again for your effort! Really appreciate it!

To double check whether the ID is actually nullable, can you share the generated Typescript interface for BuildingComponentInfo? Please also share which import you’re using for @Nullable.

Here is the generated Typescript for BuildingComponentInfo.ts

import type PropertyRef_1 from "./BuildingComponentInfo/PropertyRef.js";
import type BuildingComponentType_1 from "./BuildingComponentType.js";
interface BuildingComponentInfo {
    id: string;
    name: string;
    description: string;
    type: BuildingComponentType_1;
    property: PropertyRef_1;
}
export default BuildingComponentInfo;

And for BuildingComponentInfoModel.ts:

import { _getPropertyModel as _getPropertyModel_1, makeObjectEmptyValueCreator as makeObjectEmptyValueCreator_1, ObjectModel as ObjectModel_1, StringModel as StringModel_1 } from "@vaadin/hilla-lit-form";
import type BuildingComponentInfo_1 from "./BuildingComponentInfo.js";
import PropertyRefModel_1 from "./BuildingComponentInfo/PropertyRefModel.js";
import BuildingComponentTypeModel_1 from "./BuildingComponentTypeModel.js";
class BuildingComponentInfoModel<T extends BuildingComponentInfo_1 = BuildingComponentInfo_1> extends ObjectModel_1<T> {
    static override createEmptyValue = makeObjectEmptyValueCreator_1(BuildingComponentInfoModel);
    get id(): StringModel_1 {
        return this[_getPropertyModel_1]("id", (parent, key) => new StringModel_1(parent, key, false, { meta: { javaType: "java.lang.String" } }));
    }
    get name(): StringModel_1 {
        return this[_getPropertyModel_1]("name", (parent, key) => new StringModel_1(parent, key, false, { meta: { javaType: "java.lang.String" } }));
    }
    get description(): StringModel_1 {
        return this[_getPropertyModel_1]("description", (parent, key) => new StringModel_1(parent, key, false, { meta: { javaType: "java.lang.String" } }));
    }
    get type(): BuildingComponentTypeModel_1 {
        return this[_getPropertyModel_1]("type", (parent, key) => new BuildingComponentTypeModel_1(parent, key, false));
    }
    get property(): PropertyRefModel_1 {
        return this[_getPropertyModel_1]("property", (parent, key) => new PropertyRefModel_1(parent, key, false));
    }
}
export default BuildingComponentInfoModel;

Here is the top of the file BuildingComponentInfo.java

package xxx.xxx.xxx;

import javax.annotation.Nullable;
import java.util.UUID;

public record BuildingComponentInfo(
        @Nullable UUID id,

Thank you!

So if you look at the generated TS interface, the id property is not nullable:

interface BuildingComponentInfo {
    id: string;
    name: string;
    description: string;
    type: BuildingComponentType_1;
    property: PropertyRef_1;
}

You’re probably using the wrong Nullable annotation then. I’d suggest to review How to manage type nullability in Hilla | Vaadin and use a recommended annotation from there.

Oh my. I just changed the import to:

import org.springframework.lang.Nullable;

And now it works. Great pointer from your side.

Thanks for the great and quick help and apologies for the miss on my side.