Data format for multiple Java Records implementing same interface

Hello world,
I have a problem converting my Java Record data structure to an appropriate BSON representation to save it to the data base.

Be patient with me…

I have this interface:
* Java 21 syntax.

 public sealed interface Car permits Lambo, Ferr, Paga, Lotu {
    String lPlate;
 }

And those Records:

public record Lambo(String lPlate, Integer color) implements Car { }
public record Ferr(String lPlate, Integer maxSpeed) implements Car { }
public record Paga(String lPlate, Integer year) implements Car { }
public record Lotu(String lPlate, Integer size) implements Car { }

Now i have this Record:

public record Parking(Integer size, Car...) { }

When i try to insertOne(parking) it throws:

org.bson.codecs.configuration.CodecConfigurationException: Can’t find a codec for CodecCacheKey{clazz=class [Lmy.example.Car;, types=null}.

If you notice the class name starts with “[L” my.example.Car after some research i found that it’s because BSON having a problem to handle java arbitrary syntax “…” for Car
To overcome this i implemented CarCodecProvider:

public class CarCodecProvider implements CodecProvider {
    @Override
    @SuppressWarnings("unchecked")
    public <T> Codec<T> get(Class<T> aClass, CodecRegistry codecRegistry) {
        if (aClass == Car[].class) {
            return (Codec<T>) new CarCodec(codecRegistry);
        }
        return null;
    }
}

I had a progress but,

Now the question is how to implement “CarCodec”?
I achieved this but i stuck, see comments

public class CarCodec implements Codec<Car[]> {
    private final CodecRegistry registry;

    public RuleCodec(CodecRegistry registry) {
        this.registry = registry;
    }

    @Override
    public void encode(BsonWriter writer, Car[] cars, EncoderContext encoderContext) {
        writer.writeStartArray();
        for (Car car : cars) {
           // This part works because it using RecordCodec
           // meaning "codec" type is RecordCodec
           // but obviously it doesn't containing the data type: "Lambo, Ferr, Paga, Lotu"
            Codec codec = registry.get(car.getClass());
            encoderContext.encodeWithChildContext(codec, writer, car);
        }
        writer.writeEndArray();
    }

    @Override
    public Car[] decode(BsonReader reader, DecoderContext decoderContext) {
        List<Car> cars = new ArrayList<>();
        reader.readStartArray();
        while (reader.readBsonType().isContainer()) {
          // This part doesn't work!
            Codec codec = registry.get(Car.class);
            cars.add((Car) decoderContext.decodeWithChildContext(codec, reader));
        }
        reader.readEndArray();
        return cars.toArray(new Cars[0]);
    }

    @Override
    public Class<Car[]> getEncoderClass() {
        return Car[].class;
    }

Thank you for your patience and help.

PS: BSON RecordCodec doesn’t support the annotation “BsonDiscriminator”

I got a bit further with a few changes. Instead of using varargs for the cars in the Parking record, try using List:

public record Parking(Integer size, List<Car> cars) {
}

Then you don’t have to worry about a CodecProvider for a native array, and can just do this:

class CarCodecProvider implements CodecProvider {

    @Override
    public <T> Codec<T> get(Class<T> clazz, CodecRegistry registry) {
        if (clazz.equals(Car.class)) {
            //noinspection unchecked
            return (Codec<T>) new CarCodec(registry);
        }
        return null;
    }
}

Then comes the CarCodec:

class CarCodec implements Codec<Car> {

    private final CodecRegistry registry;

    public CarCodec(CodecRegistry registry) {
        this.registry = registry;
    }

    @SuppressWarnings({"rawtypes", "unchecked"})
    @Override
    public void encode(BsonWriter writer, Car value, EncoderContext encoderContext) {
        Codec codec = registry.get(value.getClass());
        codec.encode(writer, value, encoderContext);
    }

    @Override
    public Car decode(BsonReader reader, DecoderContext decoderContext) {
        throw new UnsupportedOperationException();
    }
    
    @Override
    public Class<Car> getEncoderClass() {
        return Car.class;
    }
}

The encode method is trivial, but without some sort of discriminator there is no straightforward way to implement the decode method. So I think this will require an enhancement to the record Codec to support BsonDiscriminator, but in itself that’s probably not enough since Car is not itself a record, and there is no Codec that just handles arbitrary interfaces.

I’m not sure the way forward, but if you’re interested in pursuing it please open an issue in our Jira project.

Regards,
Jeff

Thank you Jeff for your effort and quick response.
I am using varargs because it simplifies my code and with a small effort it saves me a lot of code elsewhere…
Regarding the “CarCodec” eventually i implemented it adding the type data to the result and it fixed the problem.
I will add it here just if anyone will need it:

public class CarCodec implements Codec<Car[]> {
    private final CodecRegistry registry;

    public CarCodec(CodecRegistry registry) {
        this.registry = registry;
    }

    @Override
    public Car[] decode(BsonReader reader, DecoderContext decoderContext) {
        List<Car> cars = new ArrayList<>();
        reader.readStartArray();
        while (reader.readBsonType().isContainer()) {
            reader.readStartDocument();
            String _type = reader.readString("_type");
            Class<? extends Car> type = switch (_type) {
                case "Lambo" -> Lambo.class;
                case "Ferr" -> Ferr.class;
                case "Paga" -> Paga.class;
                case "Lotu" -> Lotu.class;
                default -> null;
            };
            if (type != null) {
                try {
                    List<Object> fields = new ArrayList<>();
                    RecordComponent[] components = type.getRecordComponents();
                    int index = 0;
                    while(reader.readBsonType() != BsonType.END_OF_DOCUMENT && components.length > index) {
                        Codec<?> codec = registry.get(components[index].getType());
                        fields.add(codec.decode(reader, decoderContext));
                        ++index;
                    }
                    Optional<Constructor<?>> constructor = Arrays.stream(type.getConstructors())
                              .filter(con -> con.getParameterCount() == fields.size())
                              .findAny();
                    if (constructor.isPresent()) {
                         constructor.get().setAccessible(true);
                         cars.add((Car) constructor.get().newInstance(fields.toArray()));
                    }
                } catch (Throwable throwable) {
                    throw new RuntimeException(throwable);
                }
            }
            reader.readEndDocument();
        }
        reader.readEndArray();
        return cars.toArray(new Car[0]);
    }

    @Override
    public void encode(BsonWriter writer, Car[] cars, EncoderContext encoderContext) {
        writer.writeStartArray();
        for (Car car : cars) {
            writer.writeStartDocument();
            writer.writeName("_type");
            writer.writeString(car.getClass().getSimpleName());
            RecordComponent[] components = car.getClass().getRecordComponents();
            for (RecordComponent component : components) {
                writer.writeName(component.getName());
                Codec codec = registry.get(component.getType());
                try {
                    codec.encode(writer, component.getAccessor().invoke(car), encoderContext);
                } catch (Throwable throwable) {
                    throw new RuntimeException(throwable);
                }
            }
            writer.writeEndDocument();
        }
        writer.writeEndArray();
    }

    @Override
    public Class<Car[]> getEncoderClass() {
        return Car[].class;
    }
}

Regarding opening an issue i will do it eventually it is one of the language features and should be supported by my opinion.
Again thank you for your time.