Skip to content
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
7 changes: 4 additions & 3 deletions release-notes/CREDITS-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -232,18 +232,15 @@ Matteo Giordano (malteo@github)
(2.14.1)

Axel Niklasson (axelniklasson@github)

* Contributed #388: (yaml) Add `YAMLParser.Feature.PARSE_BOOLEAN_LIKE_WORDS_AS_STRINGS`
to allow parsing "boolean" words as strings instead of booleans
(2.15.0)

Niels Basjes (nielsbasjes@github)

* Contributed #415: (yaml) Use `LoaderOptions.allowDuplicateKeys` to enforce duplicate key detection
(2.15.0)

Peter Haumer (phaumer@github)

* Reported #404: (yaml) Cannot serialize YAML with Deduction-Based Polymorphism
(2.15.1)

Expand Down Expand Up @@ -295,3 +292,7 @@ Gili Tzabari (@cowwoc)
* Requested #554: (csv) Enforce, document thread-safety of `CsvSchema`
(2.19.0)

Sergio Delgado (@serandel)
* Reported #154: (yaml) YAML file with no content throws `MismatchedInputException`
when binding to Object type (POJO etc)
(2.21.0)
7 changes: 6 additions & 1 deletion release-notes/VERSION-2.x
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,12 @@ Active Maintainers:

2.21.0 (not yet released)

#10: Missing `null` writes when writing `Object[]` values
#10: (csv) Missing `null` writes when writing `Object[]` values
(reported by @georgewfraser)
(fix by @cowtowncoder, w/ Claude code)
#154: (yaml) YAML file with no content throws `MismatchedInputException`
when binding to Object type (POJO etc)
(reported by Sergio D)
(fix by @cowtowncoder, w/ Claude code)

2.20.1 (30-Oct-2025)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,22 @@ public class YAMLParser extends ParserBase
*/
public enum Feature implements FormatFeature // in 2.9
{
/**
* Feature that determines whether empty YAML documents (documents with only
* comments or whitespace, or completely empty) should be exposed as empty
* Object ({@code START_OBJECT}/{@code END_OBJECT} token pair) instead of
* causing "No content to map" error.
*<p>
* This is useful for example for deserializing to POJOs with default values,
* where an empty configuration file should create an object with all default
* values rather than failing.
*<p>
* Feature is disabled by default for backwards-compatibility.
*
* @since 2.21
*/
EMPTY_DOCUMENT_AS_EMPTY_OBJECT(false),

/**
* Feature that determines whether an empty {@link String} will be parsed
* as {@code null}. Logic is part of YAML 1.1
Expand Down Expand Up @@ -173,6 +189,22 @@ private Feature(boolean defaultState) {
*/
protected String _currentAnchor;

/**
* Flag to track whether we have seen any actual content (not just
* Document/Stream start/end events) to detect empty documents.
*
* @since 2.21
*/
protected boolean _hasContent;

/**
* Flag to track whether we're currently emitting a synthetic empty object
* for an empty document (when USE_EMPTY_OBJECT_FOR_EMPTY_DOCUMENT is enabled).
*
* @since 2.21
*/
protected boolean _emittingSyntheticEmptyObject;

/*
/**********************************************************************
/* Life-cycle
Expand Down Expand Up @@ -445,6 +477,16 @@ public JsonToken nextToken() throws IOException
return null;
}

// [dataformats-text#154]: Handle synthetic empty object for empty documents
if (_emittingSyntheticEmptyObject) {
_emittingSyntheticEmptyObject = false;
_parsingContext = _parsingContext.getParent();
JsonToken token = _updateToken(JsonToken.END_OBJECT);
// Now close since we've emitted the complete empty object
close();
return token;
}

while (true) {
Event evt;
try {
Expand Down Expand Up @@ -521,11 +563,13 @@ public JsonToken nextToken() throws IOException

// scalar values are probably the commonest:
if (evt.is(Event.ID.Scalar)) {
_hasContent = true;
return _updateToken(_decodeScalar((ScalarEvent) evt));
}

// followed by maps, then arrays
if (evt.is(Event.ID.MappingStart)) {
_hasContent = true;
Mark m = evt.getStartMark();
MappingStartEvent map = (MappingStartEvent) evt;
_currentAnchor = map.getAnchor();
Expand All @@ -536,6 +580,7 @@ public JsonToken nextToken() throws IOException
_reportError("Not expecting END_OBJECT but a value");
}
if (evt.is(Event.ID.SequenceStart)) {
_hasContent = true;
Mark m = evt.getStartMark();
_currentAnchor = ((NodeEvent)evt).getAnchor();
createChildArrayContext(m.getLine(), m.getColumn());
Expand Down Expand Up @@ -572,6 +617,14 @@ public JsonToken nextToken() throws IOException
return _updateToken(JsonToken.VALUE_STRING);
}
if (evt.is(Event.ID.StreamEnd)) { // end-of-input; force closure
// [dataformats-text#154]: If we have no content and feature is enabled,
// emit a synthetic empty object instead of null
if (!_hasContent && (_formatFeatures & Feature.EMPTY_DOCUMENT_AS_EMPTY_OBJECT.getMask()) != 0) {
_emittingSyntheticEmptyObject = true;
// Don't close yet - we need to emit END_OBJECT first
createChildObjectContext(0, 0);
return _updateToken(JsonToken.START_OBJECT);
}
close();
return _updateTokenToNull();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package com.fasterxml.jackson.dataformat.yaml.deser;

import org.junit.jupiter.api.Test;

import com.fasterxml.jackson.databind.exc.MismatchedInputException;
import com.fasterxml.jackson.dataformat.yaml.*;

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

/**
* Tests for [dataformats-text#154]: Empty YAML documents (comment-only or truly empty)
* should be deserializable to POJOs with default values
*/
public class EmptyDocumentDeser154Test extends ModuleTestBase
{
static class DataWithDefaults {
public boolean wireframe = false;
public String name = "default";
public int count = 42;
}

private final YAMLMapper MAPPER = newObjectMapper();
private final YAMLMapper MAPPER_WITH_EMPTY_AS_OBJECT = YAMLMapper.builder()
.enable(YAMLParser.Feature.EMPTY_DOCUMENT_AS_EMPTY_OBJECT)
.build();

// First, verify the default behavior (should still fail for backwards compatibility)
@Test
public void testCommentOnlyYamlFailsByDefault() throws Exception
{
String yaml = "#wireframe: true";

// Without the feature enabled, this should still throw MismatchedInputException
assertThrows(MismatchedInputException.class, () -> {
MAPPER.readValue(yaml, DataWithDefaults.class);
});
}

@Test
public void testEmptyYamlFailsByDefault() throws Exception
{
String yaml = "";

// Without the feature enabled, this should still throw MismatchedInputException
assertThrows(MismatchedInputException.class, () -> {
MAPPER.readValue(yaml, DataWithDefaults.class);
});
}

@Test
public void testWhitespaceOnlyYamlFailsByDefault() throws Exception
{
String yaml = " \n \n";

// Without the feature enabled, this should still throw MismatchedInputException
assertThrows(MismatchedInputException.class, () -> {
MAPPER.readValue(yaml, DataWithDefaults.class);
});
}

// Now test with the feature enabled - should work
@Test
public void testCommentOnlyYamlWithFeature() throws Exception
{
String yaml = "#wireframe: true";
DataWithDefaults result = MAPPER_WITH_EMPTY_AS_OBJECT.readValue(yaml, DataWithDefaults.class);

// Should get default values
assertNotNull(result);
assertFalse(result.wireframe);
assertEquals("default", result.name);
assertEquals(42, result.count);
}

@Test
public void testEmptyYamlWithFeature() throws Exception
{
String yaml = "";
DataWithDefaults result = MAPPER_WITH_EMPTY_AS_OBJECT.readValue(yaml, DataWithDefaults.class);

// Should get default values
assertNotNull(result);
assertFalse(result.wireframe);
assertEquals("default", result.name);
assertEquals(42, result.count);
}

@Test
public void testWhitespaceOnlyYamlWithFeature() throws Exception
{
String yaml = " \n \n";
DataWithDefaults result = MAPPER_WITH_EMPTY_AS_OBJECT.readValue(yaml, DataWithDefaults.class);

// Should get default values
assertNotNull(result);
assertFalse(result.wireframe);
assertEquals("default", result.name);
assertEquals(42, result.count);
}

@Test
public void testMultipleCommentsOnlyYamlWithFeature() throws Exception
{
String yaml = "# This is a comment\n# Another comment\n # Indented comment";
DataWithDefaults result = MAPPER_WITH_EMPTY_AS_OBJECT.readValue(yaml, DataWithDefaults.class);

// Should get default values
assertNotNull(result);
assertFalse(result.wireframe);
assertEquals("default", result.name);
assertEquals(42, result.count);
}

// Verify that valid YAML still works (with and without feature)
@Test
public void testValidYamlWorks() throws Exception
{
String yaml = "wireframe: true";

DataWithDefaults result1 = MAPPER.readValue(yaml, DataWithDefaults.class);
assertTrue(result1.wireframe);
assertEquals("default", result1.name);
assertEquals(42, result1.count);

DataWithDefaults result2 = MAPPER_WITH_EMPTY_AS_OBJECT.readValue(yaml, DataWithDefaults.class);
assertTrue(result2.wireframe);
assertEquals("default", result2.name);
assertEquals(42, result2.count);
}

@Test
public void testPartialYamlWithFeature() throws Exception
{
String yaml = "name: custom";
DataWithDefaults result = MAPPER_WITH_EMPTY_AS_OBJECT.readValue(yaml, DataWithDefaults.class);

// Should get mix of provided and default values
assertNotNull(result);
assertFalse(result.wireframe);
assertEquals("custom", result.name);
assertEquals(42, result.count);
}
}