Binding
Sjf4j provides a unified set of entry-point APIs, allowing data to move consistently between:
- JSON / YAML / Properties
- Raw nodes (Map, List, String, Number, Boolean, null)
- Typed nodes (POJO, JOJO, JAJO)
- OBNT representations
In this page, binding means moving data between external formats, raw nodes, typed Java objects, and OBNT nodes. Parsing is only one direction of that flow.
Binding and Conversion APIs
This section focuses on the core from... / to... APIs for binding, serialization, and cross-format conversion.
fromJson() / toJson()
Bind from JSON:
Sjf4j sjf4j = new Sjf4j();
// Create a reusable Sjf4j instance for data binding operations
Object node = sjf4j.fromJson(json);
// Default: bound into raw nodes (Map/List/String/Number/Boolean/null)
JsonObject jo = sjf4j.fromJson(json, JsonObject.class);
// Bound as JsonObject, equivalent to `JsonObject.fromJson(json)`
User user = sjf4j.fromJson(json, User.class);
// Bound into POJO or JOJO
Map<String, Object> map =
sjf4j.fromJson(json, new TypeReference<Map<String, Object>>() {});
// Supports deep generics via TypeReferenceSerialize to JSON:
String json = sjf4j.toJsonString(node);
byte[] bytes = sjf4j.toJsonBytes(node);
sjf4j.toJson(output, node);fromYaml() / toYaml()
Uses the same target-type binding rules as JSON conversion.
Object node = sjf4j.fromYaml(yaml);
String yaml2 = sjf4j.toYamlString(node);fromProperties() / toProperties()
Converts between hierarchical object data and flat Java Properties structures.
Object node = sjf4j.fromProperties(properties);
Properties properties2 = sjf4j.toProperties(node);
// {"aa":{"bb":[{"cc":"dd"}]}} → aa.bb[0].cc=ddBuilding Sjf4j
Use new Sjf4j() for framework defaults, Sjf4j.global() for the shared process-wide runtime, and Sjf4j.builder() when you need an isolated runtime with custom behavior.
Creating a runtime
Sjf4j sjf4j = new Sjf4j(); // Default instance
Sjf4j sjf4j2 = Sjf4j.global(); // Shared global instance
Sjf4j sjf4j3 = Sjf4j.builder()
.jsonFacadeProvider(Jackson3JsonFacade.provider(new JsonMapper()))
.build(); // Custom configurationSjf4j.builder() options
| Builder method | Purpose | Typical usage |
|---|---|---|
jsonFacadeProvider(...) | Select the JSON backend for this runtime. | Jackson2JsonFacade.provider(...), Jackson3JsonFacade.provider(...), GsonJsonFacade.provider(), Fastjson2JsonFacade.provider(), SimpleJsonFacade.provider() |
yamlFacadeProvider(...) | Override the YAML backend. | SnakeYamlFacade.provider() |
propertiesFacadeProvider(...) | Override the Java Properties facade. | Usually only needed for custom implementations. |
nodeFacadeProvider(...) | Override the in-memory node binding/conversion facade. | Usually only needed for custom implementations. |
streamingMode(...) | Control which streaming path the runtime prefers. | AUTO, SHARED_IO, EXCLUSIVE_IO, PLUGIN_MODULE |
defaultValueFormat(type, format) | Set the default named ValueCodec format for a value type in this runtime. | defaultValueFormat(Instant.class, "epochMillis") |
includeNulls(boolean) | Control whether JSON serialization keeps null object properties. | includeNulls(false) |
Example:
Sjf4j sjf4j = Sjf4j.builder()
.jsonFacadeProvider(Jackson2JsonFacade.provider(new ObjectMapper()))
.streamingMode(StreamingContext.StreamingMode.PLUGIN_MODULE)
.defaultValueFormat(Instant.class, "epochMillis")
.includeNulls(false)
.build();To derive a new runtime from an existing one, use Sjf4j.builder(existingSjf4j). It copies the current provider, streaming, value-format, and null-handling settings before you override them.
In-Memory Binding and Copying
fromNode() / bindNode() / deepNode()
fromNode()converts one OBNT representation into another, with isolated nested results.bindNode()binds an existing node into another target type without forcing a deep copy.deepNode()performs a full deep copy.
User user = sjf4j.fromNode(node, User.class);
// Conversion between node types
User user2 = sjf4j.bindNode(node, User.class);
// Binding may reuse nested objects/arrays/maps/lists when allowed
User user3 = sjf4j.deepNode(user);
// Produces a fully detached copyCustom Object Binding
For POJO / JOJO binding, SJF4J can customize property names, creator selection, naming strategy, property discovery strategy, ignore rules, and JOJO dynamic-property behavior.
Using @NodeProperty
@NodeProperty can be used on fields, bean methods, and creator parameters.
value: declares the primary property namealiases: accepts additional input names during readscodecName: selects a namedValueCodecvariant for that field, method-backed property, or parametercodecPattern: supplies a pattern for codecs that support parameterized formats; when both are present,codecPatternwins
class UserProfile {
private String name;
@NodeProperty(value = "user_name", aliases = {"username", "userName"})
public void setName(String name) {
this.name = name;
}
public String getName() {
return name;
}
@NodeProperty(codecName = "epochMillis")
public Instant createdAt;
@NodeProperty(codecPattern = "yyyy-MM-dd")
public LocalDate birthday;
}
UserProfile user = Sjf4j.global().fromJson(
"{\"username\":\"Ada\",\"createdAt\":1710000000000,\"birthday\":\"2026-05-18\"}",
UserProfile.class
);Method-level @NodeProperty("...") renames the whole bean property family, so the getter, setter, creator parameter, and aliases should agree on the same final JSON-facing property name.
Using @NodeCreator
Use @NodeCreator on a constructor or static factory method when an object should be created from arguments first, instead of through a no-args constructor plus setters.
Properties consumed by the creator are bound first; remaining writable properties can still be applied afterward.
class User {
final String name;
final int age;
String city;
@NodeCreator
User(@NodeProperty("name") String name,
@NodeProperty("age") int age) {
this.name = name;
this.age = age;
}
public void setCity(String city) {
this.city = city;
}
}
User user = Sjf4j.global().fromJson(
"{\"name\":\"Ada\",\"age\":18,\"city\":\"Shenzhen\"}",
User.class
);
// name/age come from the creator, city is applied afterwardIf a creator parameter does not have an explicit annotation, SJF4J can also bind by Java parameter name when the class is compiled with -parameters.
Java records are supported directly. Their canonical constructor is used automatically, so
@NodeCreatoris usually unnecessary.
Compatible annotations
SJF4J also recognizes common annotations from other JSON ecosystems during object binding:
- Jackson 2 / Jackson 3
@JsonProperty→ similar to@NodeProperty("...")@JsonAlias→ similar to@NodeProperty(aliases = {...})@JsonCreator→ similar to@NodeCreator
- Fastjson2
@JSONField(name = "...")→ similar to@NodeProperty("...")@JSONField(alternateNames = {...})→ similar to@NodeProperty(aliases = {...})@JSONCreator→ similar to@NodeCreator
This makes it easy to reuse existing models, or mix SJF4J annotations with framework-specific ones in the same class when needed.
Using @NodeBinding
@NodeBinding configures type-level binding behavior for a POJO or JOJO. It currently supports: naming, propertyStrategy, readDynamic, and writeDynamic.
naming (NamingStrategy)
Controls how Java member names are mapped to JSON property names. Supported strategies:
IDENTITY(default): use the Java member name as-isSNAKE_CASE: convert camelCase names to snake_case
@NodeBinding(naming = NamingStrategy.SNAKE_CASE)
public class User extends JsonObject {
public String userName;
public int loginCount;
}
User user = Sjf4j.global().fromJson(
"""
{"user_name":"han","login_count":2}
""",
User.class
);
assertEquals("han", user.userName);
assertEquals(2, user.loginCount);
assertEquals("han", user.getString("user_name"));
assertNull(user.getString("userName"));Naming precedence (high → low):
@NodePropertyon a field, bean method, or creator parameter@NodeBinding(naming = ...)on the type- identity naming (
userName→userName)
propertyStrategy (PropertyStrategy)
propertyStrategy controls whether properties are discovered from bean accessors, fields, or both.
BEAN_ONLY: discover properties only from bean/record accessors.FIELD_ONLY: discover properties only from declared fields, including non-public fields.- using reflective field access rather than bean getters/setters.
BEAN_FIELD(default): bean-first discovery with field fallback.- Only
publicfields are auto-discovered here; - non-public fields participate only when explicitly bound, such as with
@NodeProperty. - This is the Jackson-like default.
- Only
FIELD_BEAN: field-first discovery with bean fallback. All eligible declared fields participate, including non-public ones.
Use FIELD_ONLY when a POJO should bind directly against fields instead of JavaBean accessors.
@NodeBinding(propertyStrategy = PropertyStrategy.FIELD_ONLY)
class User {
String userName;
int loginCount;
}
User user = Sjf4j.global().fromJson(
"""
{"userName":"han","loginCount":2}
""",
User.class
);readDynamic / writeDynamic
These options apply to JOJO.
@NodeBinding(readDynamic = false)
class ReadDisabledBook extends JsonObject {
public int id;
public String name;
}
@NodeBinding(writeDynamic = false)
class WriteDisabledBook extends JsonObject {
public int id;
public String name;
}
ReadDisabledBook readBook = Sjf4j.global().fromJson(
"""
{"id":1,"name":"a","extra":2}
""",
ReadDisabledBook.class
);
assertNull(readBook.get("extra"));
WriteDisabledBook writeBook = Sjf4j.global().fromJson(
"""
{"id":1,"name":"a","extra":2}
""",
WriteDisabledBook.class
);
assertEquals(2, writeBook.getInt("extra"));
assertEquals("{\"id\":1,\"name\":\"a\"}", Sjf4j.global().toJsonString(writeBook));readDynamic = true(default): retain unknown JSON properties as dynamic JOJO properties during readingreadDynamic = false: ignore unknown JSON properties unless they bind to declared fields or propertieswriteDynamic = true(default): write dynamic JOJO properties together with declared fields or propertieswriteDynamic = false: write only declared fields or properties
Using @NodeIgnore
@NodeIgnore excludes a type or property source from SJF4J property discovery.
- On a field: that field does not participate in binding
- On a getter or setter: that accessor is ignored for the bean property family
- On a type: any property whose declared type is that class is excluded, similar to Jackson's
@JsonIgnoreType
@NodeIgnore
class InternalNote {
public String text;
}
class UserProfile {
public String name;
public InternalNote note; // excluded because the type is ignored
@NodeIgnore
public String debugOnly; // excluded as a field-backed property
private String password;
@NodeIgnore
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}If you ignore only one accessor, the property may still remain writable or readable through the other accessor or a field. Ignore all participating sources when the property should disappear completely.
Custom Node Types
SJF4J allows custom Java types to participate directly in OBNT.
- For JSON objects → use
POJO/JOJO - For JSON arrays → use
JAJO - For JSON scalar values → use
NodeValue
Using @NodeValue
Annotate with @NodeValue
@NodeValue
public static class BigDay {
private final LocalDate localDate;
public BigDay(LocalDate localDate) {
this.localDate = localDate;
}
@ValueToRaw
public String valueToRaw() {
return localDate.toString();
}
@RawToValue
public static BigDay rawToValue(String raw) {
return new BigDay(LocalDate.parse(raw));
}
@ValueCopy
public BigDay valueCopy() {
return new BigDay(localDate);
}
}The type can then be used directly without explicit registration:
BigDay day = Sjf4j.global().fromJson("\"2026-01-01\"", BigDay.class);
assertEquals("\"2026-01-01\"", Sjf4j.global().toJsonString(day));@NodeValue types also participate in named codec selection, when multiple ValueCodec variants are registered for the same logical value type.
Or register a ValueCodec
For third-party or JDK types:
NodeRegistry.registerValueCodec(new ValueCodec<LocalDate, String>() {
@Override
public Class<LocalDate> valueClass() {
return LocalDate.class;
}
@Override
public Class<String> rawClass() {
return String.class;
}
@Override
public String valueToRaw(LocalDate node) {
return node.toString();
}
@Override
public LocalDate rawToValue(String raw) {
return raw == null ? null : LocalDate.parse(raw);
}
});Named formats can then be selected at runtime. For example, Instant already has built-in iso and epochMillis formats:
Sjf4j sjf4j = Sjf4j.builder()
.defaultValueFormat(Instant.class, "epochMillis")
.build();
Instant instant = Instant.parse("2024-01-01T10:00:00Z");
assertEquals("1704103200000", sjf4j.toJsonString(instant));
assertEquals(instant, sjf4j.fromJson("1704103200000", Instant.class));Use this when one value type needs different wire formats in different runtimes or fields.
Built-in NodeValue-style codecs registered by default:
| Java type | Raw node type | Notes / built-in formats |
|---|---|---|
URI | String | URI string |
URL | String | URL string |
UUID | String | Canonical UUID string |
Locale | String | BCP 47 language tag |
Currency | String | ISO currency code |
ZoneId | String | Zone ID, such as Asia/Shanghai |
Instant | String / Long | Default string form; built-in formats: iso, epochMillis |
LocalDate | String | ISO-8601 date; supports codecPattern (e.g. yyyy-MM-dd) |
LocalTime | String | ISO-8601 local time; supports codecPattern (e.g. HH:mm:ss) |
LocalDateTime | String | ISO-8601 local date-time; supports codecPattern (e.g. yyyy-MM-dd'T'HH:mm:ss) |
OffsetDateTime | String | ISO-8601 offset date-time; supports codecPattern (e.g. yyyy-MM-dd'T'HH:mm:ssXXX) |
ZonedDateTime | String | ISO-8601 zoned date-time; supports codecPattern (e.g. yyyy-MM-dd'T'HH:mm:ssXXX'['VV']') |
Duration | String | ISO-8601 duration |
Period | String | ISO-8601 period |
Path | String | Filesystem path string |
File | String | File path string |
Pattern | String | Regular-expression pattern |
InetAddress | String | Host address string |
Date | String | Serialized via Instant string |
Calendar | String | Serialized via zoned date-time string |
Optional<T> | Object | Flattens Optional.of(x) → x, Optional.empty() → null |
Built-in codecPattern support currently applies only to these temporal codecs: LocalDate, LocalTime, LocalDateTime, OffsetDateTime, and ZonedDateTime.
Using @OneOf
@OneOf enables polymorphic binding by mapping one logical type to multiple concrete types.
It supports three practical patterns:
- Discriminator on the same object (
scope = CURRENT, default) - Discriminator from parent object (
scope = PARENT) - Fallback by JSON runtime shape (object/array), when no discriminator is provided
1) Discriminator on current object
@OneOf(key = "kind", value = {
@OneOf.Mapping(value = Cat.class, when = "cat"),
@OneOf.Mapping(value = Dog.class, when = "dog")
})
class Animal {
String kind;
String name;
}
class Cat extends Animal { int lives; }
class Dog extends Animal { int bark; }
Animal a = Sjf4j.global().fromJson(
"{\"kind\":\"dog\",\"name\":\"Lucky\",\"bark\":3}",
Animal.class
);
// a is Dog2) Discriminator from parent object
class ParentZoo {
String kind;
@OneOf(
scope = OneOf.Scope.PARENT,
key = "kind",
value = {
@OneOf.Mapping(value = Cat.class, when = "cat"),
@OneOf.Mapping(value = Dog.class, when = "dog")
},
onNoMatch = OneOf.OnNoMatch.FAILBACK_NULL
)
Animal pet;
}
ParentZoo z = Sjf4j.global().fromJson(
"{\"kind\":\"cat\",\"pet\":{\"name\":\"Mimi\",\"lives\":9}}",
ParentZoo.class
);
// z.pet is Cat
path-based parent discriminator resolution is currently not supported in streaming mode.
3) No discriminator: bind by JSON shape
@OneOf(value = {
@OneOf.Mapping(PolyObj.class),
@OneOf.Mapping(PolyArr.class)
})
interface Poly {}
class PolyObj extends JsonObject implements Poly {}
class PolyArr extends JsonArray implements Poly {}
Poly p1 = Sjf4j.global().fromJson("{\"a\":1}", Poly.class); // PolyObj
Poly p2 = Sjf4j.global().fromJson("[1,2,3]", Poly.class); // PolyArrMatching behavior
key: discriminator field namepath: discriminator JSONPath expression (supported inScope.CURRENT), evaluated only whenkeyis not providedwhen: accepted discriminator values for one mapping; runtime discriminator values are matched after string conversionscope: where to resolve discriminator (CURRENTorPARENT)onNoMatch: behavior when no mapping matches (FAILby default, orFAILBACK_NULL)
Performance
SJF4J adds structural semantics on top of underlying codecs, so runtime cost depends on both the selected backend and the target model.
- In most benchmark scenarios, SJF4J performs close to native backend levels.
JOJOusually keeps a POJO-like performance profile while adding open-model flexibility.
See Benchmarks for measured results and backend-specific notes.