9797import static io .confluent .connect .protobuf .ProtobufData .PROTOBUF_TYPE_PROP ;
9898import static io .confluent .connect .protobuf .ProtobufData .PROTOBUF_TYPE_TAG ;
9999import static io .confluent .connect .protobuf .ProtobufData .PROTOBUF_TYPE_UNION_PREFIX ;
100+ import static io .confluent .connect .protobuf .ProtobufData .PROTOBUF_TYPE_WRAPPER ;
100101import static io .confluent .kafka .serializers .protobuf .test .TimestampValueOuterClass .TimestampValue .newBuilder ;
101102import static org .junit .Assert .assertArrayEquals ;
102103import static org .junit .Assert .assertEquals ;
@@ -2436,6 +2437,10 @@ private void assertOneofBranchRoundTrips(
24362437 Struct members = flatten ? value : (Struct ) value .get ("payload" );
24372438 assertEquals (payloadValue , members .get (payloadField ));
24382439
2440+ // Write-side fidelity is only asserted for the (non-flattened) union representation. With
2441+ // flattenUnions the oneof grouping is lost on read (members become plain top-level fields),
2442+ // so the converter cannot reconstruct the oneof on write and a byte-faithful round-trip is
2443+ // not achievable by design; these flattened cases therefore verify the read side only.
24392444 if (!flatten ) {
24402445 // Write the Connect data back to protobuf. The regenerated schema must keep the oneof
24412446 // member as its original scalar type (not a google.protobuf wrapper message), and the
@@ -2449,6 +2454,133 @@ private void assertOneofBranchRoundTrips(
24492454 }
24502455 }
24512456
2457+ private static final String WRAPPER_ONEOF_SCHEMA = "syntax = \" proto3\" ;\n "
2458+ + "package io.confluent.test;\n "
2459+ + "import \" google/protobuf/wrappers.proto\" ;\n "
2460+ + "message WrapperOneofMessage {\n "
2461+ + " google.protobuf.StringValue display_name = 2;\n " // wrapper, NOT in oneof
2462+ + " google.protobuf.BoolValue verified = 3;\n " // wrapper, NOT in oneof
2463+ + " oneof payment_method {\n "
2464+ + " google.protobuf.StringValue credit_card = 7;\n " // wrapper IN oneof
2465+ + " google.protobuf.StringValue bank_account = 8;\n " // wrapper IN oneof
2466+ + " }\n "
2467+ + " oneof contact_info {\n "
2468+ + " string email = 5;\n " // plain string IN oneof
2469+ + " string phone = 6;\n " // plain string IN oneof
2470+ + " }\n "
2471+ + " oneof mixed_choice {\n "
2472+ + " google.protobuf.Int32Value count = 9;\n " // wrapper IN oneof
2473+ + " string note = 10;\n " // plain string IN oneof
2474+ + " }\n "
2475+ + "}\n " ;
2476+
2477+ @ Test
2478+ public void testWrapperTypedOneofMembersRoundTrip () throws Exception {
2479+ ProtobufDataConfig config = new ProtobufDataConfig .Builder ()
2480+ .with (ProtobufDataConfig .WRAPPER_FOR_NULLABLES_CONFIG , true )
2481+ .with (ProtobufDataConfig .GENERATE_INDEX_FOR_UNIONS_CONFIG , false )
2482+ .build ();
2483+ ProtobufData protobufData = new ProtobufData (config );
2484+ ProtobufSchema protobufSchema = new ProtobufSchema (WRAPPER_ONEOF_SCHEMA );
2485+ Descriptor descriptor = protobufSchema .toDescriptor ();
2486+
2487+ // Read side: wrapper types are unwrapped to optional scalars whether or not they are in a
2488+ // oneof. A wrapper-typed oneof member additionally carries the wrapper marker, which is how
2489+ // the write side tells it apart from a plain scalar member (both are optional scalars).
2490+ Schema connectSchema = protobufData .toConnectSchema (protobufSchema );
2491+ Schema displayName = connectSchema .field ("display_name" ).schema ();
2492+ assertEquals (Schema .Type .STRING , displayName .type ());
2493+ assertTrue (displayName .isOptional ());
2494+ assertNull (displayName .parameters ().get (PROTOBUF_TYPE_WRAPPER )); // not a oneof member
2495+
2496+ Schema creditCard = connectSchema .field ("payment_method" ).schema ().field ("credit_card" ).schema ();
2497+ assertEquals (Schema .Type .STRING , creditCard .type ());
2498+ assertEquals ("true" , creditCard .parameters ().get (PROTOBUF_TYPE_WRAPPER )); // wrapper in oneof
2499+
2500+ Schema email = connectSchema .field ("contact_info" ).schema ().field ("email" ).schema ();
2501+ assertEquals (Schema .Type .STRING , email .type ());
2502+ assertNull (email .parameters ().get (PROTOBUF_TYPE_WRAPPER )); // plain scalar in oneof
2503+
2504+ // A single oneof mixing a wrapper-typed member and a plain member: the marker is per-member,
2505+ // so only the wrapper member carries it.
2506+ Schema mixed = connectSchema .field ("mixed_choice" ).schema ();
2507+ assertEquals (Schema .Type .INT32 , mixed .field ("count" ).schema ().type ());
2508+ assertEquals ("true" , mixed .field ("count" ).schema ().parameters ().get (PROTOBUF_TYPE_WRAPPER ));
2509+ assertEquals (Schema .Type .STRING , mixed .field ("note" ).schema ().type ());
2510+ assertNull (mixed .field ("note" ).schema ().parameters ().get (PROTOBUF_TYPE_WRAPPER ));
2511+
2512+ // Each oneof branch must round-trip byte-for-byte: a wrapper member stays a wrapper message
2513+ // and a plain member stays a plain scalar.
2514+ assertWrapperOneofRoundTrips (protobufData , protobufSchema , descriptor , "credit_card" , "email" );
2515+ assertWrapperOneofRoundTrips (protobufData , protobufSchema , descriptor , "bank_account" , "phone" );
2516+
2517+ // Same, but for the two members of the single mixed oneof.
2518+ FieldDescriptor countField = descriptor .findFieldByName ("count" );
2519+ Descriptor int32ValueDesc = countField .getMessageType ();
2520+ DynamicMessage countValue = DynamicMessage .newBuilder (int32ValueDesc )
2521+ .setField (int32ValueDesc .findFieldByName ("value" ), 5 )
2522+ .build ();
2523+ assertOneofMemberRoundTrips (protobufData , protobufSchema , descriptor ,
2524+ "count" , countValue , FieldDescriptor .Type .MESSAGE );
2525+ assertOneofMemberRoundTrips (protobufData , protobufSchema , descriptor ,
2526+ "note" , "hello" , FieldDescriptor .Type .STRING );
2527+ }
2528+
2529+ private void assertOneofMemberRoundTrips (
2530+ ProtobufData protobufData ,
2531+ ProtobufSchema protobufSchema ,
2532+ Descriptor descriptor ,
2533+ String field ,
2534+ Object value ,
2535+ FieldDescriptor .Type expectedType ) {
2536+ DynamicMessage message = DynamicMessage .newBuilder (descriptor )
2537+ .setField (descriptor .findFieldByName (field ), value )
2538+ .build ();
2539+ byte [] originalBytes = message .toByteArray ();
2540+
2541+ SchemaAndValue schemaAndValue = protobufData .toConnectData (protobufSchema , message );
2542+ ConnectSchema .validateValue (schemaAndValue .schema (), schemaAndValue .value ());
2543+
2544+ ProtobufSchemaAndValue back =
2545+ protobufData .fromConnectData (schemaAndValue .schema (), schemaAndValue .value ());
2546+ Descriptor regenerated = back .getSchema ().toDescriptor ();
2547+ assertEquals (expectedType , regenerated .findFieldByName (field ).getType ());
2548+ assertArrayEquals (originalBytes , ((Message ) back .getValue ()).toByteArray ());
2549+ }
2550+
2551+ private void assertWrapperOneofRoundTrips (
2552+ ProtobufData protobufData ,
2553+ ProtobufSchema protobufSchema ,
2554+ Descriptor descriptor ,
2555+ String paymentField ,
2556+ String contactField ) {
2557+ FieldDescriptor displayNameField = descriptor .findFieldByName ("display_name" );
2558+ Descriptor stringValueDesc = displayNameField .getMessageType ();
2559+ DynamicMessage stringValue = DynamicMessage .newBuilder (stringValueDesc )
2560+ .setField (stringValueDesc .findFieldByName ("value" ), "shopper" )
2561+ .build ();
2562+ DynamicMessage paymentValue = DynamicMessage .newBuilder (stringValueDesc )
2563+ .setField (stringValueDesc .findFieldByName ("value" ), "secret" )
2564+ .build ();
2565+ DynamicMessage message = DynamicMessage .newBuilder (descriptor )
2566+ .setField (displayNameField , stringValue ) // non-oneof wrapper
2567+ .setField (descriptor .findFieldByName (paymentField ), paymentValue ) // wrapper in oneof
2568+ .setField (descriptor .findFieldByName (contactField ), "a@b.com" ) // plain string in oneof
2569+ .build ();
2570+ byte [] originalBytes = message .toByteArray ();
2571+
2572+ SchemaAndValue schemaAndValue = protobufData .toConnectData (protobufSchema , message );
2573+ ConnectSchema .validateValue (schemaAndValue .schema (), schemaAndValue .value ());
2574+
2575+ ProtobufSchemaAndValue back =
2576+ protobufData .fromConnectData (schemaAndValue .schema (), schemaAndValue .value ());
2577+ Descriptor regenerated = back .getSchema ().toDescriptor ();
2578+ // Wrapper members stay wrapper messages; plain members stay plain scalars.
2579+ assertEquals (FieldDescriptor .Type .MESSAGE , regenerated .findFieldByName (paymentField ).getType ());
2580+ assertEquals (FieldDescriptor .Type .STRING , regenerated .findFieldByName (contactField ).getType ());
2581+ assertArrayEquals (originalBytes , ((Message ) back .getValue ()).toByteArray ());
2582+ }
2583+
24522584 @ Test
24532585 public void testToConnectRecursiveSchema () {
24542586 ProtobufSchema protobufSchema = new ProtobufSchema (
0 commit comments