avrotize 2.21.1__tar.gz → 2.22.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. {avrotize-2.21.1 → avrotize-2.22.0}/PKG-INFO +1 -1
  2. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/_version.py +3 -3
  3. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotogo.py +21 -8
  4. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotojava/class_test.java.jinja +2 -4
  5. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotojava.py +67 -5
  6. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotopython/dataclass_core.jinja +11 -1
  7. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotopython.py +44 -0
  8. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotorust/dataclass_enum.rs.jinja +9 -7
  9. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotorust/dataclass_struct.rs.jinja +19 -11
  10. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotorust/dataclass_union.rs.jinja +25 -7
  11. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotorust.py +64 -26
  12. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp.py +42 -11
  13. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretodb.py +21 -0
  14. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretogo.py +38 -10
  15. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojava/pom.xml.jinja +5 -0
  16. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojava.py +28 -7
  17. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretopython/dataclass_core.jinja +1 -0
  18. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretopython.py +79 -6
  19. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots.py +4 -3
  20. {avrotize-2.21.1 → avrotize-2.22.0}/LICENSE +0 -0
  21. {avrotize-2.21.1 → avrotize-2.22.0}/README.md +0 -0
  22. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/__init__.py +0 -0
  23. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/__main__.py +0 -0
  24. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/asn1toavro.py +0 -0
  25. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotize.py +0 -0
  26. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocpp/CMakeLists.txt.jinja +0 -0
  27. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocpp/build.bat.jinja +0 -0
  28. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocpp/build.sh.jinja +0 -0
  29. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocpp/dataclass_body.jinja +0 -0
  30. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocpp/vcpkg.json.jinja +0 -0
  31. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocpp.py +0 -0
  32. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/README.md.jinja +0 -0
  33. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/class_test.cs.jinja +0 -0
  34. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/dataclass_core.jinja +0 -0
  35. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/enum_test.cs.jinja +0 -0
  36. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/project.csproj.jinja +0 -0
  37. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/project.sln.jinja +0 -0
  38. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/run_coverage.ps1.jinja +0 -0
  39. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/run_coverage.sh.jinja +0 -0
  40. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp/testproject.csproj.jinja +0 -0
  41. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsharp.py +0 -0
  42. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotocsv.py +0 -0
  43. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotodatapackage.py +0 -0
  44. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotodb.py +0 -0
  45. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotogo/go_enum.jinja +0 -0
  46. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotogo/go_helpers.jinja +0 -0
  47. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotogo/go_struct.jinja +0 -0
  48. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotogo/go_test.jinja +0 -0
  49. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotogo/go_union.jinja +0 -0
  50. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotographql.py +0 -0
  51. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotoiceberg.py +0 -0
  52. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotojava/enum_test.java.jinja +0 -0
  53. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotojava/testproject.pom.jinja +0 -0
  54. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotojs.py +0 -0
  55. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotojsons.py +0 -0
  56. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotojstruct.py +0 -0
  57. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotokusto.py +0 -0
  58. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotomd/README.md.jinja +0 -0
  59. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotomd.py +0 -0
  60. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotools.py +0 -0
  61. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotoparquet.py +0 -0
  62. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotoproto.py +0 -0
  63. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotopython/enum_core.jinja +0 -0
  64. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotopython/pyproject_toml.jinja +0 -0
  65. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotopython/test_class.jinja +0 -0
  66. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotopython/test_enum.jinja +0 -0
  67. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots/class_core.ts.jinja +0 -0
  68. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots/class_test.ts.jinja +0 -0
  69. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots/enum_core.ts.jinja +0 -0
  70. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots/gitignore.jinja +0 -0
  71. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots/index.ts.jinja +0 -0
  72. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots/package.json.jinja +0 -0
  73. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots/tsconfig.json.jinja +0 -0
  74. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotots.py +0 -0
  75. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/avrotoxsd.py +0 -0
  76. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/cddltostructure.py +0 -0
  77. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/commands.json +0 -0
  78. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/common.py +0 -0
  79. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/constants.py +0 -0
  80. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/csvtoavro.py +0 -0
  81. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/datapackagetoavro.py +0 -0
  82. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependencies/cpp/vcpkg/vcpkg.json +0 -0
  83. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependencies/cs/net90/dependencies.csproj +0 -0
  84. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependencies/go/go121/go.mod +0 -0
  85. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependencies/java/jdk21/pom.xml +0 -0
  86. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependencies/python/py312/requirements.txt +0 -0
  87. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependencies/rust/stable/Cargo.toml +0 -0
  88. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependencies/typescript/node22/package.json +0 -0
  89. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependency_resolver.py +0 -0
  90. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/dependency_version.py +0 -0
  91. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/generic/generic.avsc +0 -0
  92. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/jsonstoavro.py +0 -0
  93. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/jsonstostructure.py +0 -0
  94. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/jstructtoavro.py +0 -0
  95. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/kstructtoavro.py +0 -0
  96. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/kustotoavro.py +0 -0
  97. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/openapitostructure.py +0 -0
  98. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/parquettoavro.py +0 -0
  99. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/proto2parser.py +0 -0
  100. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/proto3parser.py +0 -0
  101. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototoavro.py +0 -0
  102. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/any.avsc +0 -0
  103. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/api.avsc +0 -0
  104. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/duration.avsc +0 -0
  105. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/field_mask.avsc +0 -0
  106. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/struct.avsc +0 -0
  107. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/timestamp.avsc +0 -0
  108. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/type.avsc +0 -0
  109. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/prototypes/wrappers.avsc +0 -0
  110. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocddl.py +0 -0
  111. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocpp/CMakeLists.txt.jinja +0 -0
  112. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocpp/build.bat.jinja +0 -0
  113. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocpp/build.sh.jinja +0 -0
  114. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocpp/dataclass_body.jinja +0 -0
  115. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocpp/vcpkg.json.jinja +0 -0
  116. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocpp.py +0 -0
  117. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/class_test.cs.jinja +0 -0
  118. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/dataclass_core.jinja +0 -0
  119. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/enum_test.cs.jinja +0 -0
  120. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/json_structure_converters.cs.jinja +0 -0
  121. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/program.cs.jinja +0 -0
  122. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/project.csproj.jinja +0 -0
  123. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/project.sln.jinja +0 -0
  124. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/testproject.csproj.jinja +0 -0
  125. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsharp/tuple_converter.cs.jinja +0 -0
  126. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretocsv.py +0 -0
  127. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretodatapackage.py +0 -0
  128. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretogo/go_enum.jinja +0 -0
  129. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretogo/go_helpers.jinja +0 -0
  130. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretogo/go_interface.jinja +0 -0
  131. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretogo/go_struct.jinja +0 -0
  132. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretogo/go_test.jinja +0 -0
  133. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretographql.py +0 -0
  134. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretoiceberg.py +0 -0
  135. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojava/choice_core.jinja +0 -0
  136. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojava/class_core.jinja +0 -0
  137. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojava/enum_core.jinja +0 -0
  138. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojava/equals_hashcode.jinja +0 -0
  139. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojava/tuple_core.jinja +0 -0
  140. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojs/class_core.js.jinja +0 -0
  141. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojs/enum_core.js.jinja +0 -0
  142. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojs/package.json.jinja +0 -0
  143. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojs/test_class.js.jinja +0 -0
  144. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojs/test_enum.js.jinja +0 -0
  145. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojs/test_runner.js.jinja +0 -0
  146. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojs.py +0 -0
  147. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretojsons.py +0 -0
  148. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretokusto.py +0 -0
  149. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretomd/README.md.jinja +0 -0
  150. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretomd.py +0 -0
  151. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretoproto.py +0 -0
  152. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretopython/enum_core.jinja +0 -0
  153. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretopython/map_alias.jinja +0 -0
  154. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretopython/pyproject_toml.jinja +0 -0
  155. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretopython/test_class.jinja +0 -0
  156. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretopython/test_enum.jinja +0 -0
  157. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretorust/dataclass_enum.rs.jinja +0 -0
  158. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretorust/dataclass_struct.rs.jinja +0 -0
  159. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretorust/dataclass_union.rs.jinja +0 -0
  160. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretorust.py +0 -0
  161. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots/class_core.ts.jinja +0 -0
  162. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots/enum_core.ts.jinja +0 -0
  163. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots/gitignore.jinja +0 -0
  164. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots/index.ts.jinja +0 -0
  165. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots/package.json.jinja +0 -0
  166. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots/test_class.ts.jinja +0 -0
  167. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretots/tsconfig.json.jinja +0 -0
  168. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/structuretoxsd.py +0 -0
  169. {avrotize-2.21.1 → avrotize-2.22.0}/avrotize/xsdtoavro.py +0 -0
  170. {avrotize-2.21.1 → avrotize-2.22.0}/pyproject.toml +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: avrotize
3
- Version: 2.21.1
3
+ Version: 2.22.0
4
4
  Summary: Tools to convert from and to Avro Schema from various other schema languages.
5
5
  Author-email: Clemens Vasters <clemensv@microsoft.com>
6
6
  Requires-Python: >=3.10
@@ -28,7 +28,7 @@ version_tuple: VERSION_TUPLE
28
28
  commit_id: COMMIT_ID
29
29
  __commit_id__: COMMIT_ID
30
30
 
31
- __version__ = version = '2.21.1'
32
- __version_tuple__ = version_tuple = (2, 21, 1)
31
+ __version__ = version = '2.22.0'
32
+ __version_tuple__ = version_tuple = (2, 22, 0)
33
33
 
34
- __commit_id__ = commit_id = 'g9b9841dd3'
34
+ __commit_id__ = commit_id = 'g5d3e04df0'
@@ -10,8 +10,15 @@ JsonNode = Dict[str, 'JsonNode'] | List['JsonNode'] | str | None
10
10
  class AvroToGo:
11
11
  """Converts Avro schema to Go structs, including JSON and Avro marshalling methods"""
12
12
 
13
+ # Go reserved keywords that cannot be used as package names
14
+ GO_RESERVED_WORDS = [
15
+ 'break', 'default', 'func', 'interface', 'select', 'case', 'defer', 'go', 'map', 'struct', 'chan',
16
+ 'else', 'goto', 'package', 'switch', 'const', 'fallthrough', 'if', 'range', 'type', 'continue', 'for',
17
+ 'import', 'return', 'var',
18
+ ]
19
+
13
20
  def __init__(self, base_package: str = '') -> None:
14
- self.base_package = base_package
21
+ self.base_package = self._safe_package_name(base_package) if base_package else base_package
15
22
  self.output_dir = os.getcwd()
16
23
  self.generated_types_avro_namespace: Dict[str, str] = {}
17
24
  self.generated_types_go_package: Dict[str, str] = {}
@@ -25,14 +32,15 @@ class AvroToGo:
25
32
  self.structs = []
26
33
  self.enums = []
27
34
 
35
+ def _safe_package_name(self, name: str) -> str:
36
+ """Converts a name to a safe Go package name"""
37
+ if name in self.GO_RESERVED_WORDS:
38
+ return f"{name}_"
39
+ return name
40
+
28
41
  def safe_identifier(self, name: str) -> str:
29
42
  """Converts a name to a safe Go identifier"""
30
- reserved_words = [
31
- 'break', 'default', 'func', 'interface', 'select', 'case', 'defer', 'go', 'map', 'struct', 'chan',
32
- 'else', 'goto', 'package', 'switch', 'const', 'fallthrough', 'if', 'range', 'type', 'continue', 'for',
33
- 'import', 'return', 'var',
34
- ]
35
- if name in reserved_words:
43
+ if name in self.GO_RESERVED_WORDS:
36
44
  return f"{name}_"
37
45
  return name
38
46
 
@@ -157,6 +165,10 @@ class AvroToGo:
157
165
  'original_name': field['name']
158
166
  } for field in avro_schema.get('fields', [])]
159
167
 
168
+ # Collect imports from field types
169
+ go_types = [f['type'] for f in fields]
170
+ imports = self.get_imports_for_definition(go_types)
171
+
160
172
  context = {
161
173
  'doc': avro_schema.get('doc', ''),
162
174
  'struct_name': go_struct_name,
@@ -166,6 +178,7 @@ class AvroToGo:
166
178
  'avro_annotation': self.avro_annotation,
167
179
  'json_match_predicates': [self.get_is_json_match_clause(f['name'], f['type']) for f in fields],
168
180
  'base_package': self.base_package,
181
+ 'imports': imports,
169
182
  }
170
183
 
171
184
  pkg_dir = os.path.join(self.output_dir, 'pkg', self.base_package)
@@ -430,7 +443,7 @@ class AvroToGo:
430
443
  def convert(self, avro_schema_path: str, output_dir: str):
431
444
  """Converts Avro schema to Go"""
432
445
  if not self.base_package:
433
- self.base_package = os.path.splitext(os.path.basename(avro_schema_path))[0]
446
+ self.base_package = self._safe_package_name(os.path.splitext(os.path.basename(avro_schema_path))[0])
434
447
 
435
448
  with open(avro_schema_path, 'r', encoding='utf-8') as file:
436
449
  schema = json.load(file)
@@ -113,8 +113,7 @@ public class {{ test_class_name }} {
113
113
  "Instances with different {{ field.field_name }} should not be equal");
114
114
  {%- elif field.is_enum %}
115
115
  // Enum - try to get a different enum value if more than one value exists
116
- {%- set simple_type = base_type.split('.')[-1] if '.' in base_type else base_type %}
117
- {{ simple_type }}[] enumValues = {{ simple_type }}.values();
116
+ {{ base_type }}[] enumValues = {{ base_type }}.values();
118
117
  if (enumValues.length > 1) {
119
118
  instance2.set{{ field.field_name | pascal }}(enumValues[(instance2.get{{ field.field_name | pascal }}().ordinal() + 1) % enumValues.length]);
120
119
  assertNotEquals(instance1, instance2,
@@ -150,8 +149,7 @@ public class {{ test_class_name }} {
150
149
  {%- if '<' in field.field_type %}
151
150
  {{ field.field_type }} testValue = {{ field.test_value }};
152
151
  {%- else %}
153
- {%- set simple_field_type = field.field_type.split('.')[-1] if '.' in field.field_type else field.field_type %}
154
- {{ simple_field_type }} testValue = {{ field.test_value }};
152
+ {{ field.field_type }} testValue = {{ field.test_value }};
155
153
  {%- endif %}
156
154
  {%- if not field.is_const %}
157
155
  instance.set{{ field.field_name | pascal }}(testValue);
@@ -1721,6 +1721,51 @@ class AvroToJava:
1721
1721
  def get_test_imports(self, fields: List) -> List[str]:
1722
1722
  """ Gets the necessary imports for the test class """
1723
1723
  imports = []
1724
+
1725
+ # Track simple names to detect conflicts
1726
+ # Map: simple_name -> list of FQNs that have that simple name
1727
+ simple_name_to_fqns: Dict[str, List[str]] = {}
1728
+
1729
+ # First pass: collect all custom type FQNs and their simple names
1730
+ for field in fields:
1731
+ inner_types = []
1732
+ if field.field_type.startswith("List<"):
1733
+ inner_type = field.field_type[5:-1]
1734
+ if inner_type.startswith("Map<"):
1735
+ start = inner_type.index('<') + 1
1736
+ end = inner_type.rindex('>')
1737
+ map_types = inner_type[start:end].split(',')
1738
+ if len(map_types) > 1:
1739
+ inner_types.append(map_types[1].strip())
1740
+ else:
1741
+ inner_types.append(inner_type)
1742
+ elif field.field_type.startswith("Map<"):
1743
+ start = field.field_type.index('<') + 1
1744
+ end = field.field_type.rindex('>')
1745
+ map_types = field.field_type[start:end].split(',')
1746
+ if len(map_types) > 1:
1747
+ inner_types.append(map_types[1].strip())
1748
+ if not field.field_type.startswith(("List<", "Map<")):
1749
+ inner_types.append(field.field_type)
1750
+ if hasattr(field, 'java_type_obj') and field.java_type_obj and field.java_type_obj.union_types:
1751
+ for union_member_type in field.java_type_obj.union_types:
1752
+ inner_types.append(union_member_type.type_name)
1753
+
1754
+ for type_to_check in inner_types:
1755
+ if type_to_check in self.generated_types_java_package and '.' in type_to_check:
1756
+ simple_name = type_to_check.split('.')[-1]
1757
+ if simple_name not in simple_name_to_fqns:
1758
+ simple_name_to_fqns[simple_name] = []
1759
+ if type_to_check not in simple_name_to_fqns[simple_name]:
1760
+ simple_name_to_fqns[simple_name].append(type_to_check)
1761
+
1762
+ # Find conflicting simple names (same simple name, different FQNs)
1763
+ conflicting_fqns: set = set()
1764
+ for simple_name, fqns in simple_name_to_fqns.items():
1765
+ if len(fqns) > 1:
1766
+ # This simple name has conflicts - mark all FQNs as conflicting
1767
+ conflicting_fqns.update(fqns)
1768
+
1724
1769
  for field in fields:
1725
1770
  # Extract inner types from generic collections
1726
1771
  inner_types = []
@@ -1772,7 +1817,8 @@ class AvroToJava:
1772
1817
  if type_to_check in self.generated_types_java_package:
1773
1818
  type_kind = self.generated_types_java_package[type_to_check]
1774
1819
  # Only import if it's a fully qualified name with a package
1775
- if '.' in type_to_check:
1820
+ # Skip imports for types with conflicting simple names - they'll use FQN
1821
+ if '.' in type_to_check and type_to_check not in conflicting_fqns:
1776
1822
  import_stmt = f"import {type_to_check};"
1777
1823
  if import_stmt not in imports:
1778
1824
  imports.append(import_stmt)
@@ -1809,10 +1855,11 @@ class AvroToJava:
1809
1855
  if java_qualified_name:
1810
1856
  if java_qualified_name in self.generated_types_java_package or java_qualified_name.split('.')[-1] in self.generated_types_java_package:
1811
1857
  member_type_kind = self.generated_types_java_package.get(java_qualified_name, self.generated_types_java_package.get(java_qualified_name.split('.')[-1], None))
1812
- # Import the class/enum
1813
- class_import = f"import {java_qualified_name};"
1814
- if class_import not in imports:
1815
- imports.append(class_import)
1858
+ # Import the class/enum only if not conflicting
1859
+ if java_qualified_name not in conflicting_fqns:
1860
+ class_import = f"import {java_qualified_name};"
1861
+ if class_import not in imports:
1862
+ imports.append(class_import)
1816
1863
  # No longer import test classes - we instantiate classes directly
1817
1864
  return imports
1818
1865
 
@@ -1920,6 +1967,21 @@ class AvroToJava:
1920
1967
  'Double': 'Double.valueOf(3.14)',
1921
1968
  'byte[]': 'new byte[] { 0x01, 0x02, 0x03 }',
1922
1969
  'Object': 'null', # Use null for Object types (Avro unions) to avoid reference equality issues
1970
+ # Java time types - use factory methods, not constructors
1971
+ 'Instant': 'java.time.Instant.now()',
1972
+ 'java.time.Instant': 'java.time.Instant.now()',
1973
+ 'LocalDate': 'java.time.LocalDate.now()',
1974
+ 'java.time.LocalDate': 'java.time.LocalDate.now()',
1975
+ 'LocalTime': 'java.time.LocalTime.now()',
1976
+ 'java.time.LocalTime': 'java.time.LocalTime.now()',
1977
+ 'LocalDateTime': 'java.time.LocalDateTime.now()',
1978
+ 'java.time.LocalDateTime': 'java.time.LocalDateTime.now()',
1979
+ 'Duration': 'java.time.Duration.ofSeconds(42)',
1980
+ 'java.time.Duration': 'java.time.Duration.ofSeconds(42)',
1981
+ 'UUID': 'java.util.UUID.randomUUID()',
1982
+ 'java.util.UUID': 'java.util.UUID.randomUUID()',
1983
+ 'BigDecimal': 'new java.math.BigDecimal("42.00")',
1984
+ 'java.math.BigDecimal': 'new java.math.BigDecimal("42.00")',
1923
1985
  }
1924
1986
 
1925
1987
  # Handle generic types
@@ -25,7 +25,7 @@ import avro.schema
25
25
  import avro.name
26
26
  import avro.io
27
27
  {%- endif %}
28
- {%- for import_type in import_types if import_type not in ['datetime.datetime', 'datetime.date', 'datetime.time', 'datetime.timedelta', 'decimal.Decimal'] %}
28
+ {%- for import_type in import_types if import_type not in ['datetime.datetime', 'datetime.date', 'datetime.time', 'datetime.timedelta', 'decimal.Decimal', 'uuid.UUID'] %}
29
29
  from {{ '.'.join(import_type.split('.')[:-1]) | lower }} import {{ import_type.split('.')[-1] }}
30
30
  {%- endfor %}
31
31
  {%- for import_type in import_types if import_type in ['datetime.datetime', 'datetime.date', 'datetime.time', 'datetime.timedelta'] %}
@@ -33,6 +33,16 @@ from {{ '.'.join(import_type.split('.')[:-1]) | lower }} import {{ import_type.s
33
33
  import datetime
34
34
  {%- endif %}
35
35
  {%- endfor %}
36
+ {%- for import_type in import_types if import_type == 'decimal.Decimal' %}
37
+ {%- if loop.first %}
38
+ import decimal
39
+ {%- endif %}
40
+ {%- endfor %}
41
+ {%- for import_type in import_types if import_type == 'uuid.UUID' %}
42
+ {%- if loop.first %}
43
+ import uuid
44
+ {%- endif %}
45
+ {%- endfor %}
36
46
 
37
47
  {% if dataclasses_json_annotation %}
38
48
  @dataclass_json(undefined=Undefined.EXCLUDE)
@@ -12,6 +12,38 @@ from avrotize.common import fullname, get_typing_args_from_string, is_generic_av
12
12
 
13
13
  INDENT = ' '
14
14
 
15
+ # Python standard library modules that should not be shadowed by package names
16
+ PYTHON_STDLIB_MODULES = {
17
+ 'abc', 'aifc', 'argparse', 'array', 'ast', 'asynchat', 'asyncio', 'asyncore',
18
+ 'atexit', 'audioop', 'base64', 'bdb', 'binascii', 'binhex', 'bisect', 'builtins',
19
+ 'bz2', 'calendar', 'cgi', 'cgitb', 'chunk', 'cmath', 'cmd', 'code', 'codecs',
20
+ 'codeop', 'collections', 'colorsys', 'compileall', 'concurrent', 'configparser',
21
+ 'contextlib', 'contextvars', 'copy', 'copyreg', 'cProfile', 'crypt', 'csv',
22
+ 'ctypes', 'curses', 'dataclasses', 'datetime', 'dbm', 'decimal', 'difflib',
23
+ 'dis', 'distutils', 'doctest', 'email', 'encodings', 'enum', 'errno', 'faulthandler',
24
+ 'fcntl', 'filecmp', 'fileinput', 'fnmatch', 'fractions', 'ftplib', 'functools',
25
+ 'gc', 'getopt', 'getpass', 'gettext', 'glob', 'graphlib', 'grp', 'gzip',
26
+ 'hashlib', 'heapq', 'hmac', 'html', 'http', 'imaplib', 'imghdr', 'imp',
27
+ 'importlib', 'inspect', 'io', 'ipaddress', 'itertools', 'json', 'keyword',
28
+ 'lib2to3', 'linecache', 'locale', 'logging', 'lzma', 'mailbox', 'mailcap',
29
+ 'marshal', 'math', 'mimetypes', 'mmap', 'modulefinder', 'multiprocessing',
30
+ 'netrc', 'nis', 'nntplib', 'numbers', 'operator', 'optparse', 'os', 'ossaudiodev',
31
+ 'pathlib', 'pdb', 'pickle', 'pickletools', 'pipes', 'pkgutil', 'platform',
32
+ 'plistlib', 'poplib', 'posix', 'posixpath', 'pprint', 'profile', 'pstats',
33
+ 'pty', 'pwd', 'py_compile', 'pyclbr', 'pydoc', 'queue', 'quopri', 'random',
34
+ 're', 'readline', 'reprlib', 'resource', 'rlcompleter', 'runpy', 'sched',
35
+ 'secrets', 'select', 'selectors', 'shelve', 'shlex', 'shutil', 'signal',
36
+ 'site', 'smtpd', 'smtplib', 'sndhdr', 'socket', 'socketserver', 'spwd',
37
+ 'sqlite3', 'ssl', 'stat', 'statistics', 'string', 'stringprep', 'struct',
38
+ 'subprocess', 'sunau', 'symtable', 'sys', 'sysconfig', 'syslog', 'tabnanny',
39
+ 'tarfile', 'telnetlib', 'tempfile', 'termios', 'test', 'textwrap', 'threading',
40
+ 'time', 'timeit', 'tkinter', 'token', 'tokenize', 'trace', 'traceback',
41
+ 'tracemalloc', 'tty', 'turtle', 'turtledemo', 'types', 'typing', 'unicodedata',
42
+ 'unittest', 'urllib', 'uu', 'uuid', 'venv', 'warnings', 'wave', 'weakref',
43
+ 'webbrowser', 'winreg', 'winsound', 'wsgiref', 'xdrlib', 'xml', 'xmlrpc',
44
+ 'zipapp', 'zipfile', 'zipimport', 'zlib', 'zoneinfo',
45
+ }
46
+
15
47
 
16
48
  def is_python_reserved_word(word: str) -> bool:
17
49
  """Checks if a word is a Python reserved word"""
@@ -25,6 +57,13 @@ def is_python_reserved_word(word: str) -> bool:
25
57
  return word in reserved_words
26
58
 
27
59
 
60
+ def safe_package_name(name: str) -> str:
61
+ """Converts a name to a safe Python package name that won't shadow stdlib"""
62
+ if name.lower() in PYTHON_STDLIB_MODULES:
63
+ return f"{name}_types"
64
+ return name
65
+
66
+
28
67
  class AvroToPython:
29
68
  """Converts Avro schema to Python data classes"""
30
69
 
@@ -167,6 +206,9 @@ class AvroToPython:
167
206
  enum_ref = self.generate_enum(avro_type, parent_package, write_file=True)
168
207
  import_types.add(enum_ref)
169
208
  return self.strip_package_from_fully_qualified_name(enum_ref)
209
+ elif avro_type['type'] == 'fixed':
210
+ # Fixed types are represented as bytes in Python
211
+ return 'bytes'
170
212
  elif avro_type['type'] == 'array':
171
213
  return f"typing.List[{self.convert_avro_type_to_python(avro_type['items'], parent_package, import_types)}]"
172
214
  elif avro_type['type'] == 'map':
@@ -611,6 +653,7 @@ def convert_avro_to_python(avro_schema_path, py_file_path, package_name='', data
611
653
  if not package_name:
612
654
  package_name = os.path.splitext(os.path.basename(avro_schema_path))[
613
655
  0].lower().replace('-', '_')
656
+ package_name = safe_package_name(package_name)
614
657
 
615
658
  avro_to_python = AvroToPython(
616
659
  package_name, dataclasses_json_annotation=dataclasses_json_annotation, avro_annotation=avro_annotation)
@@ -619,6 +662,7 @@ def convert_avro_to_python(avro_schema_path, py_file_path, package_name='', data
619
662
 
620
663
  def convert_avro_schema_to_python(avro_schema, py_file_path, package_name='', dataclasses_json_annotation=False, avro_annotation=False):
621
664
  """Converts Avro schema to Python data classes"""
665
+ package_name = safe_package_name(package_name) if package_name else package_name
622
666
  avro_to_python = AvroToPython(
623
667
  package_name, dataclasses_json_annotation=dataclasses_json_annotation, avro_annotation=avro_annotation)
624
668
  if isinstance(avro_schema, dict):
@@ -55,20 +55,22 @@ mod tests {
55
55
  use super::*;
56
56
  use rand::Rng;
57
57
 
58
+ {%- if serde_annotation %}
58
59
  #[test]
59
60
  fn test_serialize_deserialize_{{ enum_name.lower() }}() {
60
61
  {%- for symbol in symbols %}
61
62
  let instance = {{ enum_name }}::{{ symbol }};
62
- {%- if serde_annotation %}
63
63
  let json_bytes = serde_json::to_vec(&instance).unwrap();
64
64
  let deserialized_instance: {{ enum_name }} = serde_json::from_slice(&json_bytes).unwrap();
65
65
  assert_eq!(instance, deserialized_instance);
66
- {%- endif %}
67
- {%- if avro_annotation %}
68
- let avro_bytes = instance.to_byte_array("avro/binary").unwrap();
69
- let deserialized_avro_instance: {{ enum_name }} = {{ enum_name }}::from_data(&avro_bytes, "avro/binary").unwrap();
70
- assert_eq!(instance, deserialized_avro_instance);
71
- {%- endif %}
66
+ {%- endfor %}
67
+ }
68
+ {%- endif %}
69
+
70
+ #[test]
71
+ fn test_enum_variants_{{ enum_name.lower() }}() {
72
+ {%- for symbol in symbols %}
73
+ let _instance = {{ enum_name }}::{{ symbol }};
72
74
  {%- endfor %}
73
75
  }
74
76
  }
@@ -4,28 +4,28 @@ use apache_avro::Schema;
4
4
  {%- endif %}
5
5
  {%- if serde_annotation or avro_annotation %}
6
6
  use serde::{Serialize, Deserialize};
7
- {%- endif %}
8
7
  use std::io::Write;
9
8
  use flate2::write::GzEncoder;
10
9
  use flate2::read::GzDecoder;
10
+ {%- endif %}
11
11
  {%- set uses_chrono = false %}
12
12
  {%- set uses_uuid = false %}
13
13
  {%- set uses_hashmap = false%}
14
14
  {%- for field in fields %}
15
- {%- if field.type == "NaiveDate" or field.type == "NaiveTime" or field.type == "NaiveDateTime" %}
15
+ {%- if field.type == "NaiveDate" or field.type == "NaiveTime" or field.type == "NaiveDateTime" or "chrono::" in field.type %}
16
16
  {%- set uses_chrono = true %}
17
17
  {%- endif %}
18
- {%- if field.type == "Uuid" %}
18
+ {%- if field.type == "Uuid" or "uuid::" in field.type %}
19
19
  {%- set uses_uuid = true %}
20
20
  {%- endif %}
21
- {%- if field.type.startswith("HashMap<") %}
21
+ {%- if field.type.startswith("HashMap<") or "HashMap<" in field.type %}
22
22
  {%- set uses_hashmap = true %}
23
23
  {%- endif %}
24
24
  {%- endfor %}
25
25
  {%- if uses_chrono %}
26
26
  use chrono::{NaiveDate, NaiveTime, NaiveDateTime};
27
27
  {%- endif %}
28
- {%- if uuid %}
28
+ {%- if uses_uuid %}
29
29
  use uuid::Uuid;
30
30
  {%- endif %}
31
31
  {%- if uses_hashmap %}
@@ -39,7 +39,7 @@ use std::collections::HashMap;
39
39
  #[derive(Debug{%- if serde_annotation or avro_annotation %}, Serialize, Deserialize{%- endif %}, PartialEq, Clone, Default)]
40
40
  pub struct {{ struct_name }} {
41
41
  {%- for field in fields %}
42
- {%- if field.serde_rename %}
42
+ {%- if field.serde_rename and (serde_annotation or avro_annotation) %}
43
43
  #[serde(rename = "{{ field.original_name }}")]
44
44
  {%- endif %}
45
45
  pub {{ field.name }}: {{ field.type }},
@@ -55,6 +55,7 @@ lazy_static! {
55
55
  {%- endif %}
56
56
 
57
57
  impl {{ struct_name }} {
58
+ {%- if serde_annotation or avro_annotation %}
58
59
  /// Serializes the struct to a byte array based on the provided content type
59
60
  pub fn to_byte_array(&self, content_type: &str) -> Result<Vec<u8>, Box<dyn std::error::Error>> {
60
61
  let result: Vec<u8>;
@@ -70,14 +71,10 @@ impl {{ struct_name }} {
70
71
  let value = apache_avro::to_value(self).unwrap();
71
72
  result = apache_avro::to_avro_datum(&SCHEMA, value).unwrap();
72
73
  }
73
- {%- endif %}
74
- {%- if avro_annotation or serde_annotation %}
74
+ else {% endif -%}
75
75
  {
76
- {%- endif %}
77
76
  return Err(format!("unsupported media type: {}", media_type).into())
78
- {%- if avro_annotation or serde_annotation %}
79
77
  }
80
- {%- endif %}
81
78
  if media_type.ends_with("+gzip") {
82
79
  let mut encoder = GzEncoder::new(Vec::new(), flate2::Compression::default());
83
80
  encoder.write_all(&result)?;
@@ -87,7 +84,9 @@ impl {{ struct_name }} {
87
84
  return Ok(result)
88
85
  }
89
86
  }
87
+ {%- endif %}
90
88
 
89
+ {%- if serde_annotation or avro_annotation %}
91
90
  /// Deserializes the struct from a byte array based on the provided content type
92
91
  pub fn from_data(data: impl AsRef<[u8]>, content_type: &str) -> Result<Self, Box<dyn std::error::Error>> {
93
92
  let media_type = content_type.split(';').next().unwrap_or("");
@@ -114,7 +113,9 @@ impl {{ struct_name }} {
114
113
  {%- endif %}
115
114
  Err(format!("unsupported media type: {}", media_type).into())
116
115
  }
116
+ {%- endif %}
117
117
 
118
+ {%- if serde_annotation or avro_annotation %}
118
119
  /// Checks if the given JSON value matches the schema of the struct
119
120
  pub fn is_json_match(node: &serde_json::Value) -> bool {
120
121
  {%- for predicate in json_match_predicates %}
@@ -122,6 +123,7 @@ impl {{ struct_name }} {
122
123
  {%- endfor %}
123
124
  true
124
125
  }
126
+ {%- endif %}
125
127
 
126
128
  /// Returns the struct instance itself
127
129
  pub fn to_object(&self) -> &Self {
@@ -160,6 +162,12 @@ mod tests {
160
162
  assert!(instance.{{ field.name }} != {{ field_type }}::default()); // Check that {{ field.name }} is not default
161
163
  {%- elif field.type.startswith("std::collections::HashMap<")%}
162
164
  assert!(instance.{{ field.name }}.len() >= 0); // Check that {{ field.name }} is not empty
165
+ {%- elif field.is_generated_type %}
166
+ // Skip default comparison for generated types (enums/unions) as random values may match default
167
+ let _ = &instance.{{ field.name }};
168
+ {%- elif field.type in ["i8", "i16", "i32", "i64", "u8", "u16", "u32", "u64", "f32", "f64", "isize", "usize"] %}
169
+ // Skip default comparison for numeric types as random values can legitimately be 0
170
+ let _ = instance.{{ field.name }};
163
171
  {%- elif field.type != "bool" %}
164
172
  assert!(instance.{{ field.name }} != {{ field.type }}::default()); // Check that {{ field.name }} is not default
165
173
  {%- endif %}
@@ -1,4 +1,4 @@
1
- {%- if serde_annotation %}
1
+ {%- if serde_annotation or avro_annotation %}
2
2
  use serde::{self, Serialize, Deserialize};
3
3
  {%- endif %}
4
4
 
@@ -70,14 +70,25 @@ impl {{ union_enum_name }} {
70
70
 
71
71
  #[cfg(test)]
72
72
  impl {{ union_enum_name }} {
73
+ {%- set unique_fields = union_fields | selectattr('is_first_with_predicate') | list %}
73
74
  pub fn generate_random_instance() -> {{ union_enum_name }} {
74
75
  let mut rng = rand::thread_rng();
76
+ {%- if serde_annotation and unique_fields | length < union_fields | length %}
77
+ // Only pick from variants with unique JSON structures to ensure round-trip works
78
+ match rand::Rng::gen_range(&mut rng, 0..{{ unique_fields | length }}) {
79
+ {%- for union_field in unique_fields %}
80
+ {{ loop.index0 }} => {{ union_enum_name }}::{{ union_field.name }}({{ union_field.random_value }}),
81
+ {%- endfor %}
82
+ _ => panic!("Invalid random index generated"),
83
+ }
84
+ {%- else %}
75
85
  match rand::Rng::gen_range(&mut rng, 0..{{ union_fields | length }}) {
76
86
  {%- for union_field in union_fields %}
77
87
  {{ loop.index0 }} => {{ union_enum_name }}::{{ union_field.name }}({{ union_field.random_value }}),
78
88
  {%- endfor %}
79
89
  _ => panic!("Invalid random index generated"),
80
90
  }
91
+ {%- endif %}
81
92
  }
82
93
  }
83
94
 
@@ -85,21 +96,28 @@ impl {{ union_enum_name }} {
85
96
  mod tests {
86
97
  use super::*;
87
98
 
99
+ {%- if serde_annotation %}
88
100
  #[test]
89
101
  fn test_serialize_deserialize_{{ union_enum_name.lower() }}() {
90
102
  let mut rng = rand::thread_rng();
91
103
  {%- for union_field in union_fields %}
104
+ {%- if union_field.is_first_with_predicate %}
92
105
  let instance = {{ union_enum_name }}::{{ union_field.name }}({{ union_field.random_value }});
93
- {%- if serde_annotation %}
94
106
  let json_bytes = serde_json::to_vec(&instance).unwrap();
95
107
  let deserialized_instance: {{ union_enum_name }} = serde_json::from_slice(&json_bytes).unwrap();
96
108
  assert_eq!(instance, deserialized_instance);
109
+ {%- else %}
110
+ // Skip {{ union_field.name }} - structurally identical to earlier variant, would deserialize as first match
97
111
  {%- endif %}
98
- {%- if avro_annotation %}
99
- let avro_bytes = instance.to_byte_array("avro/binary").unwrap();
100
- let deserialized_avro_instance: {{ union_enum_name }} = {{ union_enum_name }}::from_data(&avro_bytes, "avro/binary").unwrap();
101
- assert_eq!(instance, deserialized_avro_instance);
102
- {%- endif %}
112
+ {%- endfor %}
113
+ }
114
+ {%- endif %}
115
+
116
+ #[test]
117
+ fn test_union_variants_{{ union_enum_name.lower() }}() {
118
+ let mut rng = rand::thread_rng();
119
+ {%- for union_field in union_fields %}
120
+ let _instance = {{ union_enum_name }}::{{ union_field.name }}({{ union_field.random_value }});
103
121
  {%- endfor %}
104
122
  }
105
123
  }
@@ -144,12 +144,15 @@ class AvroToRust:
144
144
  field_name = self.safe_identifier(snake(original_field_name))
145
145
  field_type = self.convert_avro_type_to_rust(field_name, field['type'], parent_namespace)
146
146
  serde_rename = field_name != original_field_name
147
+ # Check if this is a generated type (enum, union, or record) where random values may match default
148
+ is_generated_type = field_type in self.generated_types_rust_package or '::' in field_type
147
149
  fields.append({
148
150
  'original_name': original_field_name,
149
151
  'name': field_name,
150
152
  'type': field_type,
151
153
  'serde_rename': serde_rename,
152
- 'random_value': self.generate_random_value(field_type)
154
+ 'random_value': self.generate_random_value(field_type),
155
+ 'is_generated_type': is_generated_type
153
156
  })
154
157
 
155
158
  struct_name = self.safe_identifier(pascal(avro_schema['name']))
@@ -187,28 +190,51 @@ class AvroToRust:
187
190
  def get_is_json_match_clause(self, field_name: str, field_type: str, for_union=False) -> str:
188
191
  """Generates the is_json_match clause for a field"""
189
192
  ref = f'node[\"{field_name}\"]' if not for_union else 'node'
190
- if field_type == 'String' or field_type == 'Option<String>':
191
- return f"{ref}.is_string()"
192
- elif field_type == 'bool' or field_type == 'Option<bool>':
193
- return f"{ref}.is_boolean()"
194
- elif field_type == 'i32' or field_type == 'Option<i32>':
195
- return f"{ref}.is_i64()"
196
- elif field_type == 'i64' or field_type == 'Option<i64>':
197
- return f"{ref}.is_i64()"
198
- elif field_type == 'f32' or field_type == 'Option<f32>':
199
- return f"{ref}.is_f64()"
200
- elif field_type == 'f64' or field_type == 'Option<f64>':
201
- return f"{ref}.is_f64()"
202
- elif field_type == 'Vec<u8>' or field_type == 'Option<Vec<u8>>':
203
- return f"{ref}.is_array()"
204
- elif field_type == 'serde_json::Value' or field_type == 'std::collections::HashMap<String, String>':
205
- return f"{ref}.is_object()"
206
- elif field_type.startswith('std::collections::HashMap<String, '):
207
- return f"{ref}.is_object()"
208
- elif field_type.startswith('Vec<'):
209
- return f"{ref}.is_array()"
193
+
194
+ # Check if type is optional - if so, we need to allow null values
195
+ is_optional = field_type.startswith('Option<')
196
+ base_type = field_type[7:-1] if is_optional else field_type
197
+ null_check = f" || {ref}.is_null()" if is_optional else ""
198
+
199
+ # serde_json::Value can be any JSON type, so always return true
200
+ if base_type == 'serde_json::Value':
201
+ return "true"
202
+
203
+ if base_type == 'String':
204
+ return f"({ref}.is_string(){null_check})"
205
+ elif base_type == 'bool':
206
+ return f"({ref}.is_boolean(){null_check})"
207
+ elif base_type == 'i32':
208
+ return f"({ref}.is_i64(){null_check})"
209
+ elif base_type == 'i64':
210
+ return f"({ref}.is_i64(){null_check})"
211
+ elif base_type == 'f32':
212
+ return f"({ref}.is_f64(){null_check})"
213
+ elif base_type == 'f64':
214
+ return f"({ref}.is_f64(){null_check})"
215
+ elif base_type == 'Vec<u8>':
216
+ return f"({ref}.is_array(){null_check})"
217
+ elif base_type == 'std::collections::HashMap<String, String>':
218
+ return f"({ref}.is_object(){null_check})"
219
+ elif base_type.startswith('std::collections::HashMap<String, '):
220
+ return f"({ref}.is_object(){null_check})"
221
+ elif base_type.startswith('Vec<'):
222
+ return f"({ref}.is_array(){null_check})"
223
+ # chrono types - check for string (ISO 8601 format) or number (timestamp)
224
+ elif 'chrono::NaiveDateTime' in base_type or 'NaiveDateTime' in base_type:
225
+ return f"({ref}.is_string() || {ref}.is_i64(){null_check})"
226
+ elif 'chrono::NaiveDate' in base_type or 'NaiveDate' in base_type:
227
+ return f"({ref}.is_string() || {ref}.is_i64(){null_check})"
228
+ elif 'chrono::NaiveTime' in base_type or 'NaiveTime' in base_type:
229
+ return f"({ref}.is_string() || {ref}.is_i64(){null_check})"
230
+ # uuid type - check for string
231
+ elif 'uuid::Uuid' in base_type or 'Uuid' in base_type:
232
+ return f"({ref}.is_string(){null_check})"
210
233
  else:
211
- return f"{field_type}::is_json_match(&{ref})"
234
+ # Custom types - call their is_json_match method
235
+ if is_optional:
236
+ return f"({base_type}::is_json_match(&{ref}) || {ref}.is_null())"
237
+ return f"{base_type}::is_json_match(&{ref})"
212
238
 
213
239
 
214
240
  def generate_enum(self, avro_schema: Dict, parent_namespace: str) -> str:
@@ -250,17 +276,29 @@ class AvroToRust:
250
276
  ns = namespace.replace('.', '::').lower()
251
277
  union_enum_name = pascal(field_name) + 'Union'
252
278
  union_types = [self.convert_avro_type_to_rust(field_name + "Option" + str(i), t, namespace) for i, t in enumerate(avro_type) if t != 'null']
253
- union_fields = [
254
- {
279
+
280
+ # Track seen predicates to identify structurally identical variants
281
+ seen_predicates: set = set()
282
+ union_fields = []
283
+ for i, t in enumerate(union_types):
284
+ predicate = self.get_is_json_match_clause(field_name, t, for_union=True)
285
+ # Mark if this is the first variant with this predicate structure
286
+ # Subsequent variants with same predicate can't be distinguished during JSON deserialization
287
+ is_first_with_predicate = predicate not in seen_predicates
288
+ seen_predicates.add(predicate)
289
+ union_fields.append({
255
290
  'name': pascal(t.rsplit('::',1)[-1]),
256
291
  'type': t,
257
292
  'random_value': self.generate_random_value(t),
258
293
  'default_value': 'Default::default()',
259
- 'json_match_predicate': self.get_is_json_match_clause(field_name, t, for_union=True),
260
- } for i, t in enumerate(union_types)]
294
+ 'json_match_predicate': predicate,
295
+ 'is_first_with_predicate': is_first_with_predicate,
296
+ })
297
+
261
298
  qualified_union_enum_name = self.safe_package(self.concat_package(ns, union_enum_name))
262
299
  context = {
263
300
  'serde_annotation': self.serde_annotation,
301
+ 'avro_annotation': self.avro_annotation,
264
302
  'union_enum_name': union_enum_name,
265
303
  'union_fields': union_fields,
266
304
  'json_match_predicates': [self.get_is_json_match_clause(f['name'], f['type'], for_union=True) for f in union_fields]