Skip to content

ApplicationDescriptorDeserializer doesn't validate required JSON fields are present, causes NullPointerException #196

Description

@sfloess

Bug Description

ApplicationDescriptorDeserializer.deserialize() calls .asText(), .asInt(), and .asLong() on the result of node.get() without checking if the node exists. If any required field is missing from the JSON, node.get() returns null, and calling methods on null causes NullPointerException.

Location

  • File: jplatform-cluster/src/main/java/org/flossware/jplatform/cluster/ApplicationDescriptorJsonModule.java
  • Lines: 74-121

Problematic Code

@Override
public ApplicationDescriptor deserialize(JsonParser parser, DeserializationContext ctxt)
        throws IOException {
    JsonNode node = parser.getCodec().readTree(parser);

    ApplicationDescriptor.Builder builder = ApplicationDescriptor.builder()
            .applicationId(node.get("applicationId").asText())  // NPE if missing!
            .name(node.get("name").asText())                     // NPE if missing!
            .version(node.get("version").asText())               // NPE if missing!
            .mainClass(node.get("mainClass").asText());          // NPE if missing!
    
    // ... later ...
    
    JsonNode threadPool = node.get("threadPool");
    if (threadPool != null) {
        ThreadPoolConfig config = ThreadPoolConfig.builder()
                .corePoolSize(threadPool.get("corePoolSize").asInt())    // NPE if field missing!
                .maxPoolSize(threadPool.get("maxPoolSize").asInt())      // NPE if field missing!
                .queueCapacity(threadPool.get("queueCapacity").asInt())  // NPE if field missing!
                .keepAliveTimeSeconds(threadPool.get("keepAliveTimeSeconds").asLong())  // NPE if field missing!
                .build();
        builder.threadPoolConfig(config);
    }

    return builder.build();
}

Example Demonstrating the Bug

// Malformed JSON missing required fields
String json = "{\"applicationId\": \"app1\"}";  // Missing name, version, mainClass

ObjectMapper mapper = new ObjectMapper();
mapper.registerModule(new ApplicationDescriptorJsonModule());

// Throws NullPointerException instead of helpful error
ApplicationDescriptor descriptor = mapper.readValue(json, ApplicationDescriptor.class);
// NPE: Cannot invoke "com.fasterxml.jackson.databind.JsonNode.asText()" because the return value of "com.fasterxml.jackson.databind.JsonNode.get(String)" is null

Similar issue with threadPool nested fields:

// ThreadPool object present but missing required fields
String json = "{
    \"applicationId\": \"app1\",
    \"name\": \"App\",
    \"version\": \"1.0\",
    \"mainClass\": \"com.App\",
    \"threadPool\": {
        \"corePoolSize\": 5
        // Missing maxPoolSize, queueCapacity, keepAliveTimeSeconds
    }
}";

// NPE when accessing missing threadPool fields
ApplicationDescriptor descriptor = mapper.readValue(json, ApplicationDescriptor.class);

Impact

  • Uninformative errors: NullPointerException instead of clear validation message
  • Debugging difficulty: Stack trace doesn't indicate which field is missing
  • Cluster failures: When deserializing application state from distributed store, malformed data causes crashes
  • Security: Malformed JSON from untrusted sources can cause DoS

Proposed Fix

Add null checks with informative error messages:

@Override
public ApplicationDescriptor deserialize(JsonParser parser, DeserializationContext ctxt)
        throws IOException {
    JsonNode node = parser.getCodec().readTree(parser);

    // Validate required fields
    JsonNode appIdNode = node.get("applicationId");
    if (appIdNode == null) {
        throw new IOException("Missing required field: applicationId");
    }
    JsonNode nameNode = node.get("name");
    if (nameNode == null) {
        throw new IOException("Missing required field: name");
    }
    JsonNode versionNode = node.get("version");
    if (versionNode == null) {
        throw new IOException("Missing required field: version");
    }
    JsonNode mainClassNode = node.get("mainClass");
    if (mainClassNode == null) {
        throw new IOException("Missing required field: mainClass");
    }

    ApplicationDescriptor.Builder builder = ApplicationDescriptor.builder()
            .applicationId(appIdNode.asText())
            .name(nameNode.asText())
            .version(versionNode.asText())
            .mainClass(mainClassNode.asText());

    // ... classpath and properties ...

    // Validate nested threadPool fields if present
    JsonNode threadPool = node.get("threadPool");
    if (threadPool != null) {
        JsonNode coreSizeNode = threadPool.get("corePoolSize");
        JsonNode maxSizeNode = threadPool.get("maxPoolSize");
        JsonNode queueCapNode = threadPool.get("queueCapacity");
        JsonNode keepAliveNode = threadPool.get("keepAliveTimeSeconds");
        
        if (coreSizeNode == null || maxSizeNode == null || 
            queueCapNode == null || keepAliveNode == null) {
            throw new IOException("threadPool object is incomplete - missing required fields");
        }
        
        ThreadPoolConfig config = ThreadPoolConfig.builder()
                .corePoolSize(coreSizeNode.asInt())
                .maxPoolSize(maxSizeNode.asInt())
                .queueCapacity(queueCapNode.asInt())
                .keepAliveTimeSeconds(keepAliveNode.asLong())
                .build();
        builder.threadPoolConfig(config);
    }

    // ... rest of code ...
    
    return builder.build();
}

This provides clear error messages when required fields are missing and prevents NullPointerException.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions