Repository: provectus/kafka-ui
Issue: Produce message is not working for topics with enums in schema
Labels: type/bug
scope/backend

**Describe the bug**
Produce message is not working for topics with enums in schema. If you'll try to produce a message for a topic with schema and enum in it. Frontend will fail:

```
 Type key is missing
    at Object.a.default (index.ts:112)
    at m (index.ts:69)
    at Object.a.build [as default] (index.ts:218)
    at a.default (index.ts:15)
    at SendMessage.tsx:67
    at Rl (react-dom.production.min.js:262)
    at a.unstable_runWithPriority (scheduler.production.min.js:18)
    at Gt (react-dom.production.min.js:122)
    at Ll (react-dom.production.min.js:261)
    at react-dom.production.min.js:261
```

The problem is In json schema generator which skip type property for enum fields.


Issue comments:

Pull request review comments:

Patch that resolved the issue:
diff --git a/kafka-ui-api/pom.xml b/kafka-ui-api/pom.xml
index 001c22143ff..0209bc27459 100644
--- a/kafka-ui-api/pom.xml
+++ b/kafka-ui-api/pom.xml
@@ -170,6 +170,12 @@
             <version>${assertj.version}</version>
             <scope>test</scope>
         </dependency>
+        <dependency>
+            <groupId>com.github.java-json-tools</groupId>
+            <artifactId>json-schema-validator</artifactId>
+            <version>2.2.14</version>
+            <scope>test</scope>
+        </dependency>
 
     </dependencies>
 
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java
index d354340f080..ac12109d5a3 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverter.java
@@ -1,8 +1,10 @@
 package com.provectus.kafka.ui.util.jsonschema;
 
 import java.net.URI;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.UUID;
@@ -41,13 +43,7 @@ private FieldSchema convertField(Schema.Field field, Map<String, FieldSchema> de
 
   private FieldSchema convertSchema(String name, Schema schema,
                                     Map<String, FieldSchema> definitions, boolean ref) {
-    if (!schema.isUnion() || (schema.getTypes().size() == 2 && schema.isNullable())) {
-      if (schema.isUnion()) {
-        final Optional<Schema> firstType =
-            schema.getTypes().stream().filter(t -> !t.getType().equals(Schema.Type.NULL))
-                .findFirst();
-        schema = firstType.orElseThrow();
-      }
+    if (!schema.isUnion()) {
       JsonType type = convertType(schema);
       switch (type.getType()) {
         case BOOLEAN:
@@ -68,20 +64,29 @@ private FieldSchema convertSchema(String name, Schema schema,
         default: throw new RuntimeException("Unknown type");
       }
     } else {
-      return new OneOfFieldSchema(
-          schema.getTypes().stream()
-              .map(typeSchema ->
-                  convertSchema(
-                      name + UUID.randomUUID().toString(),
-                      typeSchema,
-                      definitions,
-                      true
-                  )
-              ).collect(Collectors.toList())
-      );
+      return createUnionSchema(schema, definitions);
     }
   }
 
+  private FieldSchema createUnionSchema(Schema schema, Map<String, FieldSchema> definitions) {
+    final Map<String, FieldSchema> fields = schema.getTypes().stream()
+        .filter(t -> !t.getType().equals(Schema.Type.NULL))
+        .map(f -> Tuples.of(
+            f.getType().getName().toLowerCase(Locale.ROOT),
+            convertSchema(
+                f.getType().getName().toLowerCase(Locale.ROOT),
+                f, definitions, true
+            )
+        )).collect(Collectors.toMap(
+            Tuple2::getT1,
+            Tuple2::getT2
+        ));
+
+    return new ObjectFieldSchema(
+        fields, Collections.emptyList()
+    );
+  }
+
   private FieldSchema createObjectSchema(String name, Schema schema,
                                          Map<String, FieldSchema> definitions, boolean ref) {
     final Map<String, FieldSchema> fields = schema.getFields().stream()
diff --git a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java
index 13ac8c8b527..715f7d5f442 100644
--- a/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java
+++ b/kafka-ui-api/src/main/java/com/provectus/kafka/ui/util/jsonschema/EnumJsonType.java
@@ -2,6 +2,7 @@
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.TextNode;
 import java.util.List;
 import java.util.Map;
 
@@ -17,7 +18,9 @@ public EnumJsonType(List<String> values) {
   @Override
   public Map<String, JsonNode> toJsonNode(ObjectMapper mapper) {
     return Map.of(
-        this.type.getName(),
+        "type",
+        new TextNode(Type.STRING.getName()),
+        Type.ENUM.getName(),
         mapper.valueToTree(values)
     );
   }


Test patch:
diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java
index 036b1c48edc..dbe37f5695b 100644
--- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java
+++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/AvroJsonSchemaConverterTest.java
@@ -1,15 +1,23 @@
 package com.provectus.kafka.ui.util.jsonschema;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.github.fge.jsonschema.core.exceptions.ProcessingException;
+import com.github.fge.jsonschema.core.report.ProcessingReport;
+import com.github.fge.jsonschema.main.JsonSchemaFactory;
+import com.provectus.kafka.ui.serde.schemaregistry.AvroMessageFormatter;
+import io.confluent.kafka.schemaregistry.avro.AvroSchemaUtils;
+import java.io.IOException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import org.apache.avro.Schema;
+import org.apache.avro.generic.GenericData;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.Test;
 
 public class AvroJsonSchemaConverterTest {
   @Test
-  public void avroConvertTest() throws URISyntaxException {
+  public void avroConvertTest() throws URISyntaxException, JsonProcessingException {
     final AvroJsonSchemaConverter converter = new AvroJsonSchemaConverter();
     URI basePath = new URI("http://example.com/");
 
@@ -72,20 +80,98 @@ public void avroConvertTest() throws URISyntaxException {
             + " }"
     );
 
-    String expected =
-            "{\"$id\":\"http://example.com/Message\","
-            + "\"$schema\":\"https://json-schema.org/draft/2020-12/schema\","
-            + "\"type\":\"object\",\"properties\":{\"record\":{\"$ref\":"
-            + "\"#/definitions/RecordInnerMessage\"}},\"required\":[\"record\"],"
-            + "\"definitions\":{\"RecordInnerMessage\":{\"type\":\"object\",\"properties\":"
-            + "{\"long_text\":{\"type\":\"string\"},\"array\":{\"type\":\"array\",\"items\":"
-            + "{\"type\":\"string\"}},\"id\":{\"type\":\"integer\"},\"text\":{\"type\":\"string\"},"
-            + "\"map\":{\"type\":\"object\",\"additionalProperties\":{\"type\":\"integer\"}},"
-            + "\"order\":{\"enum\":[\"SPADES\",\"HEARTS\",\"DIAMONDS\",\"CLUBS\"]}},"
-            + "\"required\":[\"id\",\"text\",\"order\",\"array\",\"map\"]}}}";
+
+    String expected = "{\"$id\":\"http://example.com/Message\","
+        + "\"$schema\":\"https://json-schema.org/draft/2020-12/schema\","
+        + "\"type\":\"object\",\"properties\":{\"record\":"
+        + "{\"$ref\":\"#/definitions/RecordInnerMessage\"}},"
+        + "\"required\":[\"record\"],\"definitions\":"
+        + "{\"RecordInnerMessage\":{\"type\":\"object\",\""
+        + "properties\":{\"long_text\":{\"type\":\"object\","
+        + "\"properties\":{\"string\":{\"type\":\"string\"}}},"
+        + "\"array\":{\"type\":\"array\",\"items\":{\"type\":\"string\"}},"
+        + "\"id\":{\"type\":\"integer\"},\"text\":{\"type\":\"string\"},"
+        + "\"map\":{\"type\":\"object\",\"additionalProperties\":"
+        + "{\"type\":\"integer\"}},\"order\":{\"type\":\"string\","
+        + "\"enum\":[\"SPADES\",\"HEARTS\",\"DIAMONDS\",\"CLUBS\"]}},"
+        + "\"required\":[\"id\",\"text\",\"order\",\"array\",\"map\"]}}}";
 
     final JsonSchema convertRecord = converter.convert(basePath, recordSchema);
-    Assertions.assertEquals(expected, convertRecord.toJson(new ObjectMapper()));
 
+    ObjectMapper om = new ObjectMapper();
+    Assertions.assertEquals(
+        om.readTree(expected),
+        om.readTree(
+            convertRecord.toJson(om)
+        )
+    );
+
+  }
+
+  @Test
+  public void testNullableUnions() throws URISyntaxException, IOException, ProcessingException {
+    final AvroJsonSchemaConverter converter = new AvroJsonSchemaConverter();
+    URI basePath = new URI("http://example.com/");
+    final ObjectMapper objectMapper = new ObjectMapper();
+
+    Schema recordSchema = (new Schema.Parser()).parse(
+        " {"
+            + "     \"type\": \"record\","
+            + "     \"name\": \"Message\","
+            + "     \"namespace\": \"com.provectus.kafka\","
+            + "     \"fields\": ["
+            + "                     {"
+            + "                         \"name\": \"text\","
+            + "                         \"type\": ["
+            + "                             \"null\","
+            + "                             \"string\""
+            + "                         ],"
+            + "                         \"default\": null"
+            + "                     },"
+            + "                     {"
+            + "                         \"name\": \"value\","
+            + "                         \"type\": ["
+            + "                             \"null\","
+            + "                             \"string\","
+            + "                             \"long\""
+            + "                         ],"
+            + "                         \"default\": null"
+            + "                     }"
+            + "     ]"
+            + " }"
+    );
+
+    final GenericData.Record record = new GenericData.Record(recordSchema);
+    record.put("text", "Hello world");
+    record.put("value", 100L);
+    byte[] jsonBytes = AvroSchemaUtils.toJson(record);
+    String serialized = new String(jsonBytes);
+
+    String expected =
+        "{\"$id\":\"http://example.com/Message\","
+        + "\"$schema\":\"https://json-schema.org/draft/2020-12/schema\","
+        + "\"type\":\"object\",\"properties\":{\"text\":"
+        + "{\"type\":\"object\",\"properties\":{\"string\":"
+        + "{\"type\":\"string\"}}},\"value\":{\"type\":\"object\","
+        + "\"properties\":{\"string\":{\"type\":\"string\"},"
+        + "\"long\":{\"type\":\"integer\"}}}}}";
+
+    final JsonSchema convert = converter.convert(basePath, recordSchema);
+    Assertions.assertEquals(
+        objectMapper.readTree(expected),
+        objectMapper.readTree(
+            convert.toJson(objectMapper)
+        )
+    );
+
+
+    final ProcessingReport validate =
+        JsonSchemaFactory.byDefault().getJsonSchema(
+            objectMapper.readTree(expected)
+        ).validate(
+            objectMapper.readTree(serialized)
+        );
+
+    Assertions.assertTrue(validate.isSuccess());
   }
 }
\ No newline at end of file
diff --git a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java
index fa4235d42c2..cc905460779 100644
--- a/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java
+++ b/kafka-ui-api/src/test/java/com/provectus/kafka/ui/util/jsonschema/ProtobufSchemaConverterTest.java
@@ -1,5 +1,6 @@
 package com.provectus.kafka.ui.util.jsonschema;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
 import com.fasterxml.jackson.databind.ObjectMapper;
 import io.confluent.kafka.schemaregistry.protobuf.ProtobufSchema;
 import java.net.URI;
@@ -11,7 +12,7 @@
 public class ProtobufSchemaConverterTest {
 
   @Test
-  public void testSimpleProto() throws URISyntaxException {
+  public void testSimpleProto() throws URISyntaxException, JsonProcessingException {
 
     String proto = "syntax = \"proto3\";\n"
         + "package com.acme;\n"
@@ -49,7 +50,8 @@ public void testSimpleProto() throws URISyntaxException {
         + "{\"type\":\"object\",\"properties\":"
         + "{\"optionalField\":{\"oneOf\":[{\"type\":\"string\"},"
         + "{\"type\":\"integer\"}]},\"other_id\":"
-        + "{\"type\":\"integer\"},\"order\":{\"enum\":[\"FIRST\",\"SECOND\"]}}}}}";
+        + "{\"type\":\"integer\"},\"order\":{\"enum\":[\"FIRST\",\"SECOND\"],"
+        + "\"type\":\"string\"}}}}}";
 
     ProtobufSchema protobufSchema = new ProtobufSchema(proto);
 
@@ -58,6 +60,13 @@ public void testSimpleProto() throws URISyntaxException {
 
     final JsonSchema convert =
         converter.convert(basePath, protobufSchema.toDescriptor("MyRecord"));
-    Assertions.assertEquals(expected, convert.toJson(new ObjectMapper()));
+
+    ObjectMapper om = new ObjectMapper();
+    Assertions.assertEquals(
+        om.readTree(expected),
+        om.readTree(
+            convert.toJson(om)
+        )
+    );
   }
 }
\ No newline at end of file