diff --git a/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java b/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java
index 99bbd30..f972414 100644
--- a/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java
+++ b/protoc-gen-java-optional/src/main/java/org/grpcmock/protoc/plugin/OptionalGenerator.java
@@ -18,7 +18,6 @@
 import com.salesforce.jprotoc.ProtocPlugin;
 import java.util.Collections;
 import java.util.List;
-import java.util.Locale;
 import java.util.Map;
 import java.util.Optional;
 import java.util.OptionalDouble;
@@ -43,32 +42,45 @@ public class OptionalGenerator extends Generator {
   private static final String CLASS_SCOPE = "class_scope:";
   private static final String DEFAULT_OPTIONAL_CLASS = Optional.class.getName();
   private static final String DEFAULT_OPTIONAL_GETTER_METHOD = "get";
-  private static final Map<JavaType, String> PRIMITIVE_CLASSES = ImmutableMap.<JavaType, String>builder()
-      .put(JavaType.INT, Integer.class.getSimpleName())
-      .put(JavaType.LONG, Long.class.getSimpleName())
-      .put(JavaType.FLOAT, Float.class.getSimpleName())
-      .put(JavaType.DOUBLE, Double.class.getSimpleName())
-      .put(JavaType.BOOLEAN, Boolean.class.getSimpleName())
-      .put(JavaType.STRING, String.class.getSimpleName())
-      .put(JavaType.BYTE_STRING, ByteString.class.getName())
-      .build();
-  private static final Map<String, String> PRIMITIVE_OPTIONALS = ImmutableMap.<String, String>builder()
-      .put(Integer.class.getSimpleName(), OptionalInt.class.getName())
-      .put(Long.class.getSimpleName(), OptionalLong.class.getName())
-      .put(Double.class.getSimpleName(), OptionalDouble.class.getName())
-      .build();
-  private static final Map<String, String> PRIMITIVE_OPTIONAL_GETTER_METHODS = ImmutableMap.<String, String>builder()
-      .put(Integer.class.getSimpleName(), "getAsInt")
-      .put(Long.class.getSimpleName(), "getAsLong")
-      .put(Double.class.getSimpleName(), "getAsDouble")
-      .build();
+  private static final Map<JavaType, String> PRIMITIVE_CLASSES =
+      ImmutableMap.<JavaType, String>builder()
+          .put(JavaType.INT, Integer.class.getSimpleName())
+          .put(JavaType.LONG, Long.class.getSimpleName())
+          .put(JavaType.FLOAT, Float.class.getSimpleName())
+          .put(JavaType.DOUBLE, Double.class.getSimpleName())
+          .put(JavaType.BOOLEAN, Boolean.class.getSimpleName())
+          .put(JavaType.STRING, String.class.getSimpleName())
+          .put(JavaType.BYTE_STRING, ByteString.class.getName())
+          .build();
+  private static final Map<String, String> PRIMITIVE_OPTIONALS =
+      ImmutableMap.<String, String>builder()
+          .put(Integer.class.getSimpleName(), OptionalInt.class.getName())
+          .put(Long.class.getSimpleName(), OptionalLong.class.getName())
+          .put(Double.class.getSimpleName(), OptionalDouble.class.getName())
+          .build();
+  private static final Map<String, String> PRIMITIVE_OPTIONAL_GETTER_METHODS =
+      ImmutableMap.<String, String>builder()
+          .put(Integer.class.getSimpleName(), "getAsInt")
+          .put(Long.class.getSimpleName(), "getAsLong")
+          .put(Double.class.getSimpleName(), "getAsDouble")
+          .build();
+  private Parameters parameters;
+  private ProtoTypeMap protoTypeMap;
 
   public static void main(String[] args) {
     ProtocPlugin.generate(new OptionalGenerator());
   }
 
-  private Parameters parameters;
-  private ProtoTypeMap protoTypeMap;
+  private static boolean hasFieldPresence(FieldDescriptorProto fieldDescriptor) {
+    return fieldDescriptor.getLabel() != Label.LABEL_REPEATED
+        && (fieldDescriptor.getProto3Optional()
+            || fieldDescriptor.getType() == Type.TYPE_MESSAGE
+            || fieldDescriptor.hasOneofIndex());
+  }
+
+  private static String templatePath(String path) {
+    return TEMPLATES_DIRECTORY + path;
+  }
 
   @Override
   public List<File> generateFiles(CodeGeneratorRequest request) throws GeneratorException {
@@ -89,21 +101,25 @@ protected List<Feature> supportedFeatures() {
 
   private Stream<File> handleProtoFile(FileDescriptorProto fileDescriptor) {
     String protoPackage = fileDescriptor.getPackage();
-    String javaPackage = fileDescriptor.getOptions().hasJavaPackage()
-        ? fileDescriptor.getOptions().getJavaPackage()
-        : protoPackage;
+    String javaPackage =
+        fileDescriptor.getOptions().hasJavaPackage()
+            ? fileDescriptor.getOptions().getJavaPackage()
+            : protoPackage;
 
     return fileDescriptor.getMessageTypeList().stream()
-        .flatMap(descriptor -> handleMessage(descriptor, getFileName(fileDescriptor, descriptor), protoPackage, javaPackage));
+        .flatMap(
+            descriptor ->
+                handleMessage(
+                    descriptor,
+                    getFileName(fileDescriptor, descriptor),
+                    protoPackage,
+                    javaPackage));
   }
 
   private Stream<File> handleMessage(
-      DescriptorProto messageDescriptor,
-      String fileName,
-      String protoPackage,
-      String javaPackage
-  ) {
-    String javaPackagePath = javaPackage.isEmpty() ? "" : javaPackage.replace(".", DIR_SEPARATOR) + DIR_SEPARATOR;
+      DescriptorProto messageDescriptor, String fileName, String protoPackage, String javaPackage) {
+    String javaPackagePath =
+        javaPackage.isEmpty() ? "" : javaPackage.replace(".", DIR_SEPARATOR) + DIR_SEPARATOR;
     String protoPackagePath = protoPackage.isEmpty() ? "" : protoPackage + ".";
     String filePath = javaPackagePath + fileName + JAVA_EXTENSION;
     String fullMethodName = protoPackagePath + messageDescriptor.getName();
@@ -112,13 +128,22 @@ private Stream<File> handleMessage(
         handleSingleMessage(messageDescriptor, filePath, fullMethodName),
         messageDescriptor.getNestedTypeList().stream()
             .filter(nestedDescriptor -> !nestedDescriptor.getOptions().getMapEntry())
-            .flatMap(nestedDescriptor -> handleMessage(nestedDescriptor, fileName, fullMethodName, javaPackage)));
+            .flatMap(
+                nestedDescriptor ->
+                    handleMessage(nestedDescriptor, fileName, fullMethodName, javaPackage)));
   }
 
-  private Stream<File> handleSingleMessage(DescriptorProto messageDescriptor, String filePath, String fullMethodName) {
+  private Stream<File> handleSingleMessage(
+      DescriptorProto messageDescriptor, String filePath, String fullMethodName) {
     return Stream.of(
-            createFile(messageDescriptor, filePath, fullMethodName, BUILDER_SCOPE, this::createBuilderMethods),
-            createFile(messageDescriptor, filePath, fullMethodName, CLASS_SCOPE, this::createClassMethods))
+            createFile(
+                messageDescriptor,
+                filePath,
+                fullMethodName,
+                BUILDER_SCOPE,
+                this::createBuilderMethods),
+            createFile(
+                messageDescriptor, filePath, fullMethodName, CLASS_SCOPE, this::createClassMethods))
         .filter(Optional::isPresent)
         .map(Optional::get);
   }
@@ -128,24 +153,29 @@ private Optional<File> createFile(
       String fileName,
       String fullMethodName,
       String scopeType,
-      Function<FieldDescriptorProto, Optional<String>> createMethods
-  ) {
+      Function<FieldDescriptorProto, Optional<String>> createMethods) {
     return messageDescriptor.getFieldList().stream()
         .map(createMethods)
         .filter(Optional::isPresent)
         .map(Optional::get)
         .collect(Collectors.collectingAndThen(Collectors.joining(DELIMITER), Optional::of))
         .filter(value -> !value.isEmpty())
-        .map(methodsContent -> File.newBuilder()
-            .setName(fileName)
-            .setContent(methodsContent + DELIMITER)
-            .setInsertionPoint(scopeType + fullMethodName)
-            .build());
+        .map(
+            methodsContent ->
+                File.newBuilder()
+                    .setName(fileName)
+                    .setContent(methodsContent + DELIMITER)
+                    .setInsertionPoint(scopeType + fullMethodName)
+                    .build());
   }
 
   private Optional<String> createBuilderMethods(FieldDescriptorProto fieldDescriptor) {
     if (hasFieldPresence(fieldDescriptor)) {
-      return Stream.of(setOrClearMethod(fieldDescriptor), optionalSetOrClearMethod(fieldDescriptor), optionalGet(fieldDescriptor))
+      return Stream.of(
+              setOrClearMethod(fieldDescriptor),
+              optionalSetOrClearMethod(fieldDescriptor),
+              optionalGet(fieldDescriptor),
+              optionalGetNullable(fieldDescriptor))
           .filter(Optional::isPresent)
           .map(Optional::get)
           .collect(Collectors.collectingAndThen(Collectors.joining(DELIMITER), Optional::of));
@@ -155,7 +185,10 @@ private Optional<String> createBuilderMethods(FieldDescriptorProto fieldDescript
 
   private Optional<String> createClassMethods(FieldDescriptorProto fieldDescriptor) {
     if (hasFieldPresence(fieldDescriptor)) {
-      return optionalGet(fieldDescriptor);
+      return Stream.of(optionalGet(fieldDescriptor), optionalGetNullable(fieldDescriptor))
+          .filter(Optional::isPresent)
+          .map(Optional::get)
+          .collect(Collectors.collectingAndThen(Collectors.joining(DELIMITER), Optional::of));
     }
     return Optional.empty();
   }
@@ -164,10 +197,11 @@ private Optional<String> setOrClearMethod(FieldDescriptorProto fieldDescriptor)
     if (!parameters.isSetterObject()) {
       return Optional.empty();
     }
-    Map<?, ?> context = ImmutableMap.builder()
-        .put(METHOD_NAME, getJavaMethodName(fieldDescriptor))
-        .put(FIELD_TYPE, getJavaTypeName(fieldDescriptor))
-        .build();
+    Map<?, ?> context =
+        ImmutableMap.builder()
+            .put(METHOD_NAME, getJavaMethodName(fieldDescriptor))
+            .put(FIELD_TYPE, getJavaTypeName(fieldDescriptor))
+            .build();
     return Optional.of(applyTemplate(templatePath("setOrClear.mustache"), context));
   }
 
@@ -177,13 +211,14 @@ private Optional<String> optionalSetOrClearMethod(FieldDescriptorProto fieldDesc
     }
 
     String javaTypeName = getJavaTypeName(fieldDescriptor);
-    Map<?, ?> context = ImmutableMap.builder()
-        .put(METHOD_NAME, getJavaMethodName(fieldDescriptor))
-        .put(FIELD_TYPE, javaTypeName)
-        .put(OPTIONAL_CLASS, getOptionalClassName(javaTypeName))
-        .put(PRIMITIVE_OPTIONAL, isPrimitiveOptional(javaTypeName))
-        .put(OPTIONAL_GETTER_METHOD, getOptionalGetterMethod(javaTypeName))
-        .build();
+    Map<?, ?> context =
+        ImmutableMap.builder()
+            .put(METHOD_NAME, getJavaMethodName(fieldDescriptor))
+            .put(FIELD_TYPE, javaTypeName)
+            .put(OPTIONAL_CLASS, getOptionalClassName(javaTypeName))
+            .put(PRIMITIVE_OPTIONAL, isPrimitiveOptional(javaTypeName))
+            .put(OPTIONAL_GETTER_METHOD, getOptionalGetterMethod(javaTypeName))
+            .build();
     return Optional.of(applyTemplate(templatePath("optionalSetOrClear.mustache"), context));
   }
 
@@ -193,16 +228,34 @@ private Optional<String> optionalGet(FieldDescriptorProto fieldDescriptor) {
     }
 
     String javaTypeName = getJavaTypeName(fieldDescriptor);
-    Map<?, ?> context = ImmutableMap.builder()
-        .put(METHOD_NAME, getJavaMethodName(fieldDescriptor))
-        .put(FIELD_TYPE, javaTypeName)
-        .put(OPTIONAL_CLASS, getOptionalClassName(javaTypeName))
-        .put(PRIMITIVE_OPTIONAL, isPrimitiveOptional(javaTypeName))
-        .build();
+    Map<?, ?> context =
+        ImmutableMap.builder()
+            .put(METHOD_NAME, getJavaMethodName(fieldDescriptor))
+            .put(FIELD_TYPE, javaTypeName)
+            .put(OPTIONAL_CLASS, getOptionalClassName(javaTypeName))
+            .put(PRIMITIVE_OPTIONAL, isPrimitiveOptional(javaTypeName))
+            .build();
     return Optional.of(applyTemplate(templatePath("optionalGet.mustache"), context));
   }
 
-  private String getFileName(FileDescriptorProto fileDescriptor, DescriptorProto messageDescriptor) {
+  private Optional<String> optionalGetNullable(FieldDescriptorProto fieldDescriptor) {
+    if (!parameters.isGetterOptional()) {
+      return Optional.empty();
+    }
+
+    String javaTypeName = getJavaTypeName(fieldDescriptor);
+    Map<?, ?> context =
+        ImmutableMap.builder()
+            .put(METHOD_NAME, getJavaMethodName(fieldDescriptor))
+            .put(FIELD_TYPE, javaTypeName)
+            .put(OPTIONAL_CLASS, getOptionalClassName(javaTypeName))
+            .put(PRIMITIVE_OPTIONAL, isPrimitiveOptional(javaTypeName))
+            .build();
+    return Optional.of(applyTemplate(templatePath("optionalGetNullable.mustache"), context));
+  }
+
+  private String getFileName(
+      FileDescriptorProto fileDescriptor, DescriptorProto messageDescriptor) {
     if (fileDescriptor.getOptions().getJavaMultipleFiles()) {
       return messageDescriptor.getName();
     }
@@ -214,11 +267,14 @@ private String getFileName(FileDescriptorProto fileDescriptor, DescriptorProto m
     return Optional.ofNullable(protoTypeMap.toJavaTypeName(protoTypeName))
         .map(javaType -> javaType.substring(0, javaType.lastIndexOf('.')))
         .map(javaType -> javaType.substring(javaType.lastIndexOf('.') + 1))
-        .orElseThrow(() -> new IllegalArgumentException("Failed to find filename for proto '" + fileDescriptor.getName() + "'"));
+        .orElseThrow(
+            () ->
+                new IllegalArgumentException(
+                    "Failed to find filename for proto '" + fileDescriptor.getName() + "'"));
   }
 
   private String getJavaMethodName(FieldDescriptorProto fieldDescriptor) {
-    return fieldDescriptor.getJsonName().substring(0, 1).toUpperCase(Locale.ROOT) + fieldDescriptor.getJsonName().substring(1);
+    return underscoresToCamelCase(fieldDescriptor.getName(), true, false);
   }
 
   private String getJavaTypeName(FieldDescriptorProto fieldDescriptor) {
@@ -228,10 +284,16 @@ private String getJavaTypeName(FieldDescriptorProto fieldDescriptor) {
           .map(FieldDescriptor.Type::valueOf)
           .map(FieldDescriptor.Type::getJavaType)
           .map(PRIMITIVE_CLASSES::get)
-          .orElseThrow(() -> new IllegalArgumentException("Failed to find java type for field:\n" + fieldDescriptor));
+          .orElseThrow(
+              () ->
+                  new IllegalArgumentException(
+                      "Failed to find java type for field:\n" + fieldDescriptor));
     }
     return Optional.ofNullable(protoTypeMap.toJavaTypeName(protoTypeName))
-        .orElseThrow(() -> new IllegalArgumentException("Failed to find java type for prototype '" + protoTypeName + "'"));
+        .orElseThrow(
+            () ->
+                new IllegalArgumentException(
+                    "Failed to find java type for prototype '" + protoTypeName + "'"));
   }
 
   private String getOptionalClassName(String javaTypeName) {
@@ -242,7 +304,8 @@ private String getOptionalClassName(String javaTypeName) {
 
   private String getOptionalGetterMethod(String javaTypeName) {
     return parameters.isUsePrimitiveOptionals()
-        ? PRIMITIVE_OPTIONAL_GETTER_METHODS.getOrDefault(javaTypeName, DEFAULT_OPTIONAL_GETTER_METHOD)
+        ? PRIMITIVE_OPTIONAL_GETTER_METHODS.getOrDefault(
+            javaTypeName, DEFAULT_OPTIONAL_GETTER_METHOD)
         : DEFAULT_OPTIONAL_GETTER_METHOD;
   }
 
@@ -250,14 +313,63 @@ private boolean isPrimitiveOptional(String javaTypeName) {
     return parameters.isUsePrimitiveOptionals() && PRIMITIVE_OPTIONALS.containsKey(javaTypeName);
   }
 
-  private static boolean hasFieldPresence(FieldDescriptorProto fieldDescriptor) {
-    return fieldDescriptor.getLabel() != Label.LABEL_REPEATED
-        && (fieldDescriptor.getProto3Optional()
-        || fieldDescriptor.getType() == Type.TYPE_MESSAGE
-        || fieldDescriptor.hasOneofIndex());
-  }
+  private String underscoresToCamelCase(
+      String input, boolean capNextLetter, boolean preservePeriod) {
+    StringBuilder result = new StringBuilder();
 
-  private static String templatePath(String path) {
-    return TEMPLATES_DIRECTORY + path;
+    // Note:  I distrust ctype.h due to locales.
+    for (int i = 0; i < input.length(); i++) {
+      char c = input.charAt(i);
+      if ('a' <= c && c <= 'z') {
+        if (capNextLetter) {
+          result.append((char) (c + ('A' - 'a')));
+        } else {
+          result.append(c);
+        }
+        capNextLetter = false;
+      } else if ('A' <= c && c <= 'Z') {
+        if (i == 0 && !capNextLetter) {
+          // Force first letter to lower-case unless explicitly told to
+          // capitalize it.
+          result.append((char) (c + ('a' - 'A')));
+        } else {
+          // Capital letters after the first are left as-is.
+          result.append(c);
+        }
+        capNextLetter = false;
+      } else if ('0' <= c && c <= '9') {
+        result.append(c);
+        capNextLetter = true;
+      } else {
+        capNextLetter = true;
+        if (c == '.' && preservePeriod) {
+          result.append(c);
+        }
+      }
+    }
+    // Add a trailing "_" if the name should be altered.
+    if (input.length() > 0 && input.charAt(input.length() - 1) == '#') {
+      result.append('_');
+    }
+
+    // https://github.com/protocolbuffers/protobuf/issues/8101
+    // To avoid generating invalid identifiers - if the input string
+    // starts with _<digit> (or multiple underscores then digit) then
+    // we need to preserve the underscore as an identifier cannot start
+    // with a digit.
+    // This check is being done after the loop rather than before
+    // to handle the case where there are multiple underscores before the
+    // first digit. We let them all be consumed so we can see if we would
+    // start with a digit.
+    // Note: not preserving leading underscores for all otherwise valid identifiers
+    // so as to not break anything that relies on the existing behaviour
+    if (result.length() > 0
+        && '0' <= result.charAt(0)
+        && result.charAt(0) <= '9'
+        && input.length() > 0
+        && input.charAt(0) == '_') {
+      result.insert(0, '_');
+    }
+    return result.toString();
   }
 }
diff --git a/protoc-gen-java-optional/src/main/resources/templates/optionalGetNullable.mustache b/protoc-gen-java-optional/src/main/resources/templates/optionalGetNullable.mustache
new file mode 100644
index 0000000..04c8b52
--- /dev/null
+++ b/protoc-gen-java-optional/src/main/resources/templates/optionalGetNullable.mustache
@@ -0,0 +1,7 @@
+public {{#primitiveOptional}}{{/primitiveOptional}}{{^primitiveOptional}}{{javaFieldType}}{{/primitiveOptional}} getOrNull{{javaMethodName}}() {
+  if (has{{javaMethodName}}()) {
+    return get{{javaMethodName}}();
+  } else {
+    return null;
+  }
+}