Skip to content

Add failOnNullPrimitives option, default to allowing NULL #356

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Mar 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package org.example.other;

import io.avaje.jsonb.Json;

@Json
public record MyPrimitives(boolean a, int b, long c, double d) {};
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package org.example;

import io.avaje.json.JsonDataException;
import io.avaje.jsonb.JsonType;
import io.avaje.jsonb.Jsonb;
import org.example.other.MyPrimitives;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;

class FailOnNullPrimitivesTest {

@Test
void default_allowsNulls_expect_defaultValues() {
String jsonContent = "{\"a\":null,\"b\":null,\"c\":null,\"d\":null}";

// default skips unknown
Jsonb jsonb = Jsonb.builder().build();
JsonType<MyPrimitives> jsonType = jsonb.type(MyPrimitives.class);

MyPrimitives bean = jsonType.fromJson(jsonContent);
assertThat(bean.a()).isEqualTo(false);
assertThat(bean.b()).isEqualTo(0);
assertThat(bean.c()).isEqualTo(0);
assertThat(bean.d()).isEqualTo(0);
}

@Test
void failOnNullPrimitives() {
Jsonb jsonb = Jsonb.builder().failOnNullPrimitives(true).build();
JsonType<MyPrimitives> jsonType = jsonb.type(MyPrimitives.class);

assertThatThrownBy(() ->jsonType.fromJson("{\"a\":null,\"b\":null,\"c\":null,\"d\":null}"))
.isInstanceOf(JsonDataException.class)
.hasMessageContaining("Read NULL value for boolean");

assertThatThrownBy(() ->jsonType.fromJson("{\"a\":false,\"b\":null,\"c\":null,\"d\":null}"))
.isInstanceOf(JsonDataException.class)
.hasMessageContaining("Read NULL value for int");

assertThatThrownBy(() ->jsonType.fromJson("{\"a\":false,\"b\":7,\"c\":null,\"d\":null}"))
.isInstanceOf(JsonDataException.class)
.hasMessageContaining("Read NULL value for long");

assertThatThrownBy(() ->jsonType.fromJson("{\"a\":false,\"b\":7,\"c\":7,\"d\":null}"))
.isInstanceOf(JsonDataException.class)
.hasMessageContaining("Read NULL value for double");

MyPrimitives result = jsonType.fromJson("{\"a\":true,\"b\":3,\"c\":5,\"d\":7}");
assertThat(result.a()).isTrue();
assertThat(result.b()).isEqualTo(3);
assertThat(result.c()).isEqualTo(5);
assertThat(result.d()).isEqualTo(7);
}

}
3 changes: 3 additions & 0 deletions json-core/src/main/java/io/avaje/json/stream/JsonStream.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@ interface Builder {
/** Set to true to fail on unknown properties. Defaults to false. */
Builder failOnUnknown(boolean failOnUnknown);

/** Set to true to fail on NULL for primitive types. Defaults to false. */
Builder failOnNullPrimitives(boolean failOnNullPrimitives);

/** Determines how byte buffers are recycled */
Builder bufferRecycling(BufferRecycleStrategy strategy);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,20 @@ final class CoreJsonStream implements JsonStream {
private final boolean serializeNulls;
private final boolean serializeEmpty;
private final boolean failOnUnknown;
private final boolean failOnNullPrimitives;
private final BufferRecycler recycle;

/** Create additionally providing the jsonFactory. */
CoreJsonStream(
boolean serializeNulls,
boolean serializeEmpty,
boolean failOnUnknown,
boolean failOnNullPrimitives,
BufferRecycleStrategy recycle) {
this.serializeNulls = serializeNulls;
this.serializeEmpty = serializeEmpty;
this.failOnUnknown = failOnUnknown;
this.failOnNullPrimitives = failOnNullPrimitives;
this.recycle = init2Recycler(recycle);
}

Expand Down Expand Up @@ -66,7 +69,7 @@ public JsonReader reader(String json) {
@Override
public JsonReader reader(byte[] json) {
JsonParser parser = recycle.parser(json);
return new JsonReadAdapter(parser, recycle, failOnUnknown);
return new JsonReadAdapter(parser, recycle, failOnUnknown, failOnNullPrimitives);
}

@Override
Expand All @@ -77,7 +80,7 @@ public JsonReader reader(Reader reader) {
@Override
public JsonReader reader(InputStream inputStream) {
JsonParser parser = recycle.parser(inputStream);
return new JsonReadAdapter(parser, recycle, failOnUnknown);
return new JsonReadAdapter(parser, recycle, failOnUnknown, failOnNullPrimitives);
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -372,11 +372,13 @@ char[] readSimpleQuote() {

@Override
public int readInt() {
if (isNullValue()) return 0;
return NumberParser.deserializeInt(this);
}

@Override
public long readLong() {
if (isNullValue()) return 0L;
return NumberParser.deserializeLong(this);
}

Expand All @@ -387,6 +389,7 @@ public short readShort() {

@Override
public double readDouble() {
if (isNullValue()) return 0D;
return NumberParser.deserializeDouble(this);
}

Expand All @@ -404,7 +407,7 @@ public BigInteger readBigInteger() {
public boolean readBoolean() {
if (wasTrue()) {
return true;
} else if (wasFalse()) {
} else if (wasFalse() || isNullValue()) {
return false;
}
throw newParseErrorAt("Found invalid boolean value", 0);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.avaje.json.stream.core;

import io.avaje.json.JsonDataException;
import io.avaje.json.JsonReader;
import io.avaje.json.PropertyNames;

Expand All @@ -10,11 +11,13 @@ final class JsonReadAdapter implements JsonReader {

private final JsonParser reader;
private final boolean failOnUnknown;
private final boolean failOnNullPrimitives;
private final BufferRecycler recycler;

JsonReadAdapter(JsonParser reader, BufferRecycler recycler, boolean failOnUnknown) {
JsonReadAdapter(JsonParser reader, BufferRecycler recycler, boolean failOnUnknown, boolean failOnNullPrimitives) {
this.reader = reader;
this.failOnUnknown = failOnUnknown;
this.failOnNullPrimitives = failOnNullPrimitives;
this.recycler = recycler;
}

Expand Down Expand Up @@ -88,21 +91,25 @@ public String nextField() {

@Override
public boolean readBoolean() {
if (failOnNullPrimitives && reader.isNullValue()) throw new JsonDataException("Read NULL value for boolean");
return reader.readBoolean();
}

@Override
public int readInt() {
if (failOnNullPrimitives && reader.isNullValue()) throw new JsonDataException("Read NULL value for int");
return reader.readInt();
}

@Override
public long readLong() {
if (failOnNullPrimitives && reader.isNullValue()) throw new JsonDataException("Read NULL value for long");
return reader.readLong();
}

@Override
public double readDouble() {
if (failOnNullPrimitives && reader.isNullValue()) throw new JsonDataException("Read NULL value for double");
return reader.readDouble();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ public final class JsonStreamBuilder implements JsonStream.Builder {
private boolean serializeNulls;
private boolean serializeEmpty;
private boolean failOnUnknown;
private boolean failOnNullPrimitives;

/**
* Set to true to serialize nulls. Defaults to false.
Expand Down Expand Up @@ -40,6 +41,12 @@ public JsonStreamBuilder failOnUnknown(boolean failOnUnknown) {
return this;
}

@Override
public JsonStreamBuilder failOnNullPrimitives(boolean failOnNullPrimitives) {
this.failOnNullPrimitives = failOnNullPrimitives;
return this;
}

/**
* Determines how byte buffers are recycled
*/
Expand All @@ -54,6 +61,6 @@ public JsonStreamBuilder bufferRecycling(BufferRecycleStrategy strategy) {
*/
@Override
public JsonStream build() {
return new CoreJsonStream(serializeNulls, serializeEmpty, failOnUnknown, strategy);
return new CoreJsonStream(serializeNulls, serializeEmpty, failOnUnknown, failOnNullPrimitives, strategy);
}
}
167 changes: 167 additions & 0 deletions json-core/src/test/java/io/avaje/json/mapper/PrimitivesTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package io.avaje.json.mapper;

import io.avaje.json.JsonDataException;
import io.avaje.json.JsonReader;
import io.avaje.json.stream.JsonStream;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertEquals;

class PrimitivesTest {

static final JsonStream defaultJsonStream = JsonStream.builder().build();
static final JsonStream strictJsonStream = JsonStream.builder().failOnNullPrimitives(true).build();

@Test
void booleanTest() {
String input = "{\"a\":true, \"b\": false}";

try (JsonReader reader = defaultJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertTrue(reader.readBoolean());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertFalse(reader.readBoolean());
reader.endObject();
}
}

@Test
void booleanNullTest() {
String input = "{\"a\":true, \"b\": null, \"c\": 7}";

try (JsonReader reader = defaultJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertTrue(reader.readBoolean());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertFalse(reader.readBoolean());
assertTrue(reader.hasNextField());
assertEquals("c", reader.nextField());
assertEquals(7, reader.readInt());
reader.endObject();
}
}

@Test
void intNullTest() {
String input = "{\"a\":3, \"b\": null, \"c\": 7}";

try (JsonReader reader = defaultJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertEquals(3, reader.readInt());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertEquals(0, reader.readInt());
assertTrue(reader.hasNextField());
assertEquals("c", reader.nextField());
assertEquals(7, reader.readInt());
reader.endObject();
}
}

@Test
void longNullTest() {
String input = "{\"a\":3, \"b\": null, \"c\": 7}";

try (JsonReader reader = defaultJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertEquals(3, reader.readLong());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertEquals(0, reader.readLong());
assertTrue(reader.hasNextField());
assertEquals("c", reader.nextField());
assertEquals(7, reader.readLong());
reader.endObject();
}
}

@Test
void doubleNullTest() {
String input = "{\"a\":3, \"b\": null, \"c\": 7}";

try (JsonReader reader = defaultJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertEquals(3, reader.readLong());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertEquals(0, reader.readDouble());
assertTrue(reader.hasNextField());
assertEquals("c", reader.nextField());
assertEquals(7, reader.readLong());
reader.endObject();
}
}

@Test
void readBoolean_strictMode_failOnNull() {
String input = "{\"a\":true, \"b\": null, \"c\": 7}";

try (JsonReader reader = strictJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertTrue(reader.readBoolean());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertThrows(JsonDataException.class, reader::readBoolean);
}
}

@Test
void readInt_strictMode_failOnNull() {
String input = "{\"a\":3, \"b\": null, \"c\": 7}";

try (JsonReader reader = strictJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertEquals(3, reader.readInt());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertThrows(JsonDataException.class, reader::readInt);
}
}

@Test
void readLong_strictMode_failOnNull() {
String input = "{\"a\":3, \"b\": null, \"c\": 7}";

try (JsonReader reader = strictJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertEquals(3, reader.readLong());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertThrows(JsonDataException.class, reader::readLong);
}
}

@Test
void readDouble_strictMode_failOnNull() {
String input = "{\"a\":3, \"b\": null, \"c\": 7}";

try (JsonReader reader = strictJsonStream.reader(input)) {
reader.beginObject();
assertTrue(reader.hasNextField());
assertEquals("a", reader.nextField());
assertEquals(3, reader.readLong());
assertTrue(reader.hasNextField());
assertEquals("b", reader.nextField());
assertThrows(JsonDataException.class, reader::readDouble);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ void via_jreader() {
jr.process(bytes, bytes.length);

JsonReadAdapter reader =
new JsonReadAdapter(jr, ThreadLocalPool.shared(), true);
new JsonReadAdapter(jr, ThreadLocalPool.shared(), true, true);
readExampleWithAsserts(reader);
reader.close();
}
Expand Down
Loading