Skip to content

Add support of oneOf(primitive|object) in the json parser generator#2972

Merged
aleksandr-gringauz merged 4 commits into
developfrom
aleksandr-gringauz/boxed-primitive-generation
Oct 31, 2025
Merged

Add support of oneOf(primitive|object) in the json parser generator#2972
aleksandr-gringauz merged 4 commits into
developfrom
aleksandr-gringauz/boxed-primitive-generation

Conversation

@aleksandr-gringauz

@aleksandr-gringauz aleksandr-gringauz commented Oct 30, 2025

Copy link
Copy Markdown
Contributor

What does this PR do?

This PR addresses this issue.

It implements support for the following case:

"oneOf": [
      {
        "type": "string",
        "description": "string element"
      },
      {
        "type": "integer",
        "description": "integer element"
      }
]

Options of oneOf can also be of primitive types.

The best way to look at this PR is to start with the changes in the unit tests of the generator and then to read the comments to the PR explaining specific places in code.

I will update RUM schema to the latest version in a separate PR.

Motivation

What inspired you to submit this pull request?

Additional Notes

Anything else we should know when reviewing?

Review checklist (to be filled by reviewers)

  • Feature or bugfix MUST have appropriate tests (unit, integration, e2e)
  • Make sure you discussed the feature or bugfix with the maintaining team in an Issue
  • Make sure each commit and the PR mention the Issue number (cf the CONTRIBUTING doc)

@aleksandr-gringauz aleksandr-gringauz changed the title Add support of oneOf(primitive|object types) in the json parser generator Add support of oneOf(primitive|object) in the json parser generator Oct 30, 2025
@datadog-datadog-prod-us1

datadog-datadog-prod-us1 Bot commented Oct 30, 2025

Copy link
Copy Markdown

🎯 Code Coverage
Patch Coverage: 100.00%
Total Coverage: 70.82% (-0.09%)

View detailed report

This comment will be updated automatically if new data arrives.
🔗 Commit SHA: 1585433 | Docs | Datadog PR Page | Was this helpful? Give us feedback!

@codecov-commenter

codecov-commenter commented Oct 30, 2025

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 70.84%. Comparing base (ee38a9a) to head (1585433).
⚠️ Report is 1317 commits behind head on develop.

Additional details and impacted files
@@             Coverage Diff             @@
##           develop    #2972      +/-   ##
===========================================
- Coverage    70.91%   70.84%   -0.07%     
===========================================
  Files          829      829              
  Lines        30381    30381              
  Branches      5184     5184              
===========================================
- Hits         21543    21522      -21     
- Misses        7373     7385      +12     
- Partials      1465     1474       +9     

see 29 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@aleksandr-gringauz
aleksandr-gringauz force-pushed the aleksandr-gringauz/boxed-primitive-generation branch 3 times, most recently from 5bac644 to dd8cc53 Compare October 30, 2025 14:48
@aleksandr-gringauz
aleksandr-gringauz force-pushed the aleksandr-gringauz/boxed-primitive-generation branch from dd8cc53 to 67dcf0d Compare October 30, 2025 15:42

@JvmStatic
@Throws(JsonParseException::class)
public fun fromJsonObject(jsonElement: JsonElement): Path {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The json element corresponding to oneOf now is either JsonObject or JsonPrimitive. Their common parent is JsonElement.

null
}
val asBoolean = try {
if (jsonElement is JsonPrimitive) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First checking jsonElement is JsonPrimitive, then trying to parse a Boolean

null
}
val asPoint = try {
if (jsonElement is JsonObject) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First checking jsonElement is JsonObject, then trying to parse a Point

data class OneOfClass(
val name: String,
val options: List<Class>,
val options: List<Option>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Now options of oneOf can be either a class or a primitive.

import com.squareup.kotlinpoet.TypeSpec
import com.squareup.kotlinpoet.jvm.throws

class OneOfPrimitiveOptionGenerator(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This class is responsible for generating code of primitive options of oneOf. It is used in MultiClassGenerator.

Code for class options of oneOf is generated in MultiClassGenerator as before.

@Throws(JsonParseException::class)
public fun fromJsonObject(jsonObject: JsonPrimitive): String {
try {
if (jsonObject.isString) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isString check is important. Otherwise is jsonObject contains Int or Boolean jsonObject.asString will successfully "parse" it. This isn't what we want.

https://github.com/google/gson/blob/72537db9a6095720aca3a67abe77ec5a16200081/gson/src/main/java/com/google/gson/JsonPrimitive.java#L163

@Throws(JsonParseException::class)
public fun fromJsonObject(jsonObject: JsonPrimitive): Boolean {
try {
if (jsonObject.isBoolean) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isBoolean check is important. Otherwise .asBoolean it will try doing this.

@Throws(JsonParseException::class)
public fun fromJsonObject(jsonObject: JsonPrimitive): Long {
try {
if (jsonObject.isNumber) {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This isNumber check is important. Otherwise .asLong will try to extract a number from a string link.

Comment thread buildSrc/src/test/resources/input/path_array_with_number.json
@aleksandr-gringauz
aleksandr-gringauz marked this pull request as ready for review October 30, 2025 15:43
@aleksandr-gringauz
aleksandr-gringauz requested review from a team as code owners October 30, 2025 15:43
0xnm
0xnm previously approved these changes Oct 31, 2025

@0xnm 0xnm left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice!

companion object
fun fromJson(kotlin.String): Vital
fun fromJsonObject(com.google.gson.JsonObject): Vital
fun fromJsonObject(com.google.gson.JsonElement): Vital

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we probably need to rename fromJsonObject to fromJsonElement then, but such change will break Flutter, for example https://github.com/search?q=repo%3ADataDog%2Fdd-sdk-flutter%20fromJsonObject&type=code

@aleksandr-gringauz aleksandr-gringauz Oct 31, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.

I will try to rename, but I think we should rename fromJsonObject to fromJsonElement only in places where JsonElement is passed as an argument.

We can also rename here to fromJsonPrimitive (this file is introduced in the current PR).

but such change will break Flutter

LogEvent isn't affected by my change. It is still parsed from jsonObject, so no need to rename its method.

IIUC the usage in flutter that you mentioned is the only place where we use fromJsonObject link. So it should be safe to rename.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to fromJsonElement and fromJsonPrimitive where needed

ClassNameRef.IllegalStateException,
ClassNameRef.UnsupportedOperationException
)
JsonPrimitiveType.DOUBLE -> error("Double is not supported")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

maybe add comment why?

@aleksandr-gringauz aleksandr-gringauz Oct 31, 2025

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, I can entirely remove JsonPrimitiveType.DOUBLE from the code.

It is never used, we don't assign it anywhere when we parse the json link.

And JSON Schema actually has only integer and number. https://json-schema.org/understanding-json-schema/reference/numeric

I don't know why JsonPrimitiveType.DOUBLE was introduced in the first place.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed JsonPrimitiveType.DOUBLE from the codebase

ambushwork
ambushwork previously approved these changes Oct 31, 2025
@aleksandr-gringauz
aleksandr-gringauz dismissed stale reviews from ambushwork and 0xnm via 7cf8f4c October 31, 2025 11:40
@aleksandr-gringauz
aleksandr-gringauz force-pushed the aleksandr-gringauz/boxed-primitive-generation branch from 7cf8f4c to 1585433 Compare October 31, 2025 12:14
rootTypeName: String
) {
val opt = if (nullable) "?" else ""
beginControlFlow("$assignee = $getter$opt.asJsonObject$opt.let")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an important place that I missed. It corresponds to the situation when a oneOf is a field of some object.

When we are deserializing a oneOf, we need to treat it as JsonElement now. But here it was cast to JsonObject and it failed if the option of oneOf was of primitive type.

My new tests only tested the case when oneOf(primitive) was an element of an array.

I modified an old test here to test this scenario.

Comment thread buildSrc/src/test/resources/input/path_array_with_number.json
ClassNameRef.IllegalStateException,
ClassNameRef.UnsupportedOperationException
)
JsonPrimitiveType.DOUBLE -> error("Double is not supported")

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed JsonPrimitiveType.DOUBLE from the codebase

companion object
fun fromJson(kotlin.String): Vital
fun fromJsonObject(com.google.gson.JsonObject): Vital
fun fromJsonObject(com.google.gson.JsonElement): Vital

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

renamed to fromJsonElement and fromJsonPrimitive where needed

@aleksandr-gringauz
aleksandr-gringauz merged commit df659af into develop Oct 31, 2025
26 of 27 checks passed
@aleksandr-gringauz
aleksandr-gringauz deleted the aleksandr-gringauz/boxed-primitive-generation branch October 31, 2025 16:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants