diff --git a/docs/user-guide/Testing.md b/docs/user-guide/Testing.md index 628a664..947d7e6 100644 --- a/docs/user-guide/Testing.md +++ b/docs/user-guide/Testing.md @@ -85,6 +85,7 @@ During each Test run a `speccov.json` file will be created in the logs directory "status_code": "400", "status_text": "BAD REQUEST", "error_message": "{\n \"errors\": {\n \"id\": \"'5882' is not of type 'integer'\"\n },\n \"message\": \"Input payload validation failed\"\n}\n", + "request_order": 4, "sample_request": { "request_sent_timestamp": null, "response_received_timestamp": "2021-03-31 18:20:14", @@ -103,7 +104,9 @@ During each Test run a `speccov.json` file will be created in the logs directory ], "response_body": "{\n \"errors\": {\n \"id\": \"'5882' is not of type 'integer'\"\n },\n \"message\": \"Input payload validation failed\"\n}\n" }, - "request_order": 4 + "tracked_parameters": { + "per_page_9": "2" + } }, ``` @@ -129,6 +132,10 @@ the coverage data is being reported. * The __"error_message"__ value will be set to the response body if the request was not "valid". * The __"request_order"__ value is the 0 indexed order that the request was sent. * Requests sent during "preprocessing" or "postprocessing" will explicitely say so. +* The __"tracked_parameters"__ property is optional and generated only when using +`Test` mode with ```test-all-combinations```. +This property contains key-value pairs of all of the parameters +for which more than one value is being tested. #### Postprocessing Scripts: The `utilities` directory contains a sub-directory called `speccovparsing` that contains scripts for postprocessing speccov files. diff --git a/restler/checkers/checker_base.py b/restler/checkers/checker_base.py index ff3f458..8d38065 100644 --- a/restler/checkers/checker_base.py +++ b/restler/checkers/checker_base.py @@ -110,7 +110,7 @@ class CheckerBase: @rtype : Tuple(HttpResponse, HttpResponse) """ - rendered_data, parser = request.render_current(self._req_collection.candidate_values_pool) + rendered_data, parser, tracked_parameters = request.render_current(self._req_collection.candidate_values_pool) rendered_data = seq.resolve_dependencies(rendered_data) response = self._send_request(parser, rendered_data) response_to_parse = response diff --git a/restler/checkers/invalid_dynamic_object_checker.py b/restler/checkers/invalid_dynamic_object_checker.py index 66a5a8c..a6c7a3c 100644 --- a/restler/checkers/invalid_dynamic_object_checker.py +++ b/restler/checkers/invalid_dynamic_object_checker.py @@ -58,7 +58,7 @@ class InvalidDynamicObjectChecker(CheckerBase): InvalidDynamicObjectChecker.generation_executed_requests[generation].add(last_request.hex_definition) # Get the current rendering of the sequence, which will be the valid rendering of the last request - last_rendering, last_request_parser = last_request.render_current(self._req_collection.candidate_values_pool) + last_rendering, last_request_parser, tracked_parameters = last_request.render_current(self._req_collection.candidate_values_pool) # Execute the sequence up until the last request new_seq = self._execute_start_of_sequence() diff --git a/restler/checkers/namespace_rule_checker.py b/restler/checkers/namespace_rule_checker.py index 897b331..e6693d5 100644 --- a/restler/checkers/namespace_rule_checker.py +++ b/restler/checkers/namespace_rule_checker.py @@ -74,7 +74,7 @@ class NameSpaceRuleChecker(CheckerBase): self._checker_log.checker_print("\nRe-rendering start of original sequence") for request in seq.requests[:-1]: - rendered_data, parser = request.render_current( + rendered_data, parser, tracked_parameters = request.render_current( self._req_collection.candidate_values_pool ) rendered_data = seq.resolve_dependencies(rendered_data) @@ -102,8 +102,8 @@ class NameSpaceRuleChecker(CheckerBase): # Check if last request contains any trigger_object last_request = self._sequence.last_request - last_rendering, last_parser = last_request.render_current(self._req_collection.candidate_values_pool) - + last_rendering, last_parser, _ = last_request.render_current(self._req_collection.candidate_values_pool) + last_request_contains_a_trigger_object = False for obj in self._trigger_objects: if last_rendering.find(obj) != -1: @@ -118,7 +118,7 @@ class NameSpaceRuleChecker(CheckerBase): if not self._trigger_on_dynamic_objects: return # Here, trigger_on_dynamic_objects is True. - # Exit the checker if there are no consumed_types + # Exit the checker if there are no consumed_types # # in the entire sequence. if not consumed_types: return @@ -178,7 +178,7 @@ class NameSpaceRuleChecker(CheckerBase): for i in range(stopping_length): request = self._sequence.requests[i] - rendered_data, parser = request.render_current( + rendered_data, parser, tracked_parameters = request.render_current( self._req_collection.candidate_values_pool ) rendered_data = self._sequence.resolve_dependencies(rendered_data) @@ -202,7 +202,7 @@ class NameSpaceRuleChecker(CheckerBase): """ self._checker_log.checker_print("Hijack request rendering") - rendered_data, parser = req.render_current( + rendered_data, parser, tracked_parameters = req.render_current( self._req_collection.candidate_values_pool ) rendered_data = self._sequence.resolve_dependencies(rendered_data) diff --git a/restler/checkers/payload_body_checker.py b/restler/checkers/payload_body_checker.py index 4e4b6db..853f1b0 100644 --- a/restler/checkers/payload_body_checker.py +++ b/restler/checkers/payload_body_checker.py @@ -1109,7 +1109,7 @@ class PayloadBodyChecker(CheckerBase): cnt = 0 # iterate through different value combinations - for rendered_data, parser in new_request.render_iter( + for rendered_data, parser,_ in new_request.render_iter( self._req_collection.candidate_values_pool ): # check time budget diff --git a/restler/checkers/use_after_free_checker.py b/restler/checkers/use_after_free_checker.py index bd31462..1b8f992 100644 --- a/restler/checkers/use_after_free_checker.py +++ b/restler/checkers/use_after_free_checker.py @@ -109,7 +109,7 @@ class UseAfterFreeChecker(CheckerBase): """ request = seq.last_request - for rendered_data, parser in\ + for rendered_data, parser,_ in\ request.render_iter(self._req_collection.candidate_values_pool, skip=request._current_combination_id): # Hold the lock (because other workers may be rendering the same diff --git a/restler/engine/core/requests.py b/restler/engine/core/requests.py index ad0fb15..bc1b34b 100644 --- a/restler/engine/core/requests.py +++ b/restler/engine/core/requests.py @@ -102,6 +102,7 @@ class SmokeTestStats(object): self.status_text = None self.sample_request = RenderedRequestStats() + self.tracked_parameters = {} def set_matching_prefix(self, sequence_prefix): # Set the prefix of the request, if it exists. @@ -136,6 +137,12 @@ class SmokeTestStats(object): self.error_msg = response_body self.set_matching_prefix(renderings.sequence.prefix) + # Set tracked parameters + last_req = renderings.sequence.last_request + + # extract the custom payloads and enums + for property_name, property_value in last_req._tracked_parameters.items(): + self.tracked_parameters[property_name] = property_value class Request(object): """ Request Class. """ @@ -166,6 +173,7 @@ class Request(object): self._produces = set() self._set_constraints() self._create_once_requests = [] + self._tracked_parameters = {} # Check for empty request before assigning ids if self._definition: @@ -535,7 +543,7 @@ class Request(object): @type preprocessing: Bool @return: (rendered request's payload, response's parser function) - @rtype : (Str, Function Pointer) + @rtype : (Str, Function Pointer, List[Str]) """ def _raise_dict_err(type, tag): @@ -574,11 +582,30 @@ class Request(object): parser = self.metadata['post_send']['parser'] fuzzable = [] + # The following list will contain name-value pairs of properties whose combinations + # are tracked for coverage reporting purposes. + # First, in the loop below, the index of the property in the values list will be added. + # Then, at the time of returning the specific combination of values, a new list with + # the values will be created + tracked_parameters = {} for request_block in definition: primitive_type = request_block[0] - default_val = request_block[1] - quoted = request_block[2] - examples = request_block[3] + if primitive_type == primitives.FUZZABLE_GROUP: + field_name = request_block[1] + default_val = request_block[2] + quoted = request_block[3] + examples = request_block[4] + elif primitive_type in [ primitives.CUSTOM_PAYLOAD, + primitives.CUSTOM_PAYLOAD_HEADER, + primitives.CUSTOM_PAYLOAD_UUID4_SUFFIX ]: + field_name = request_block[1] + quoted = request_block[2] + examples = request_block[3] + else: + field_name = None + default_val = request_block[1] + quoted = request_block[2] + examples = request_block[3] values = [] # Handling dynamic primitives that need fresh rendering every time @@ -617,40 +644,40 @@ class Request(object): elif primitive_type == primitives.CUSTOM_PAYLOAD: try: current_fuzzable_values = candidate_values_pool.\ - get_candidate_values(primitive_type, request_id=self._request_id, tag=default_val, quoted=quoted) + get_candidate_values(primitive_type, request_id=self._request_id, tag=field_name, quoted=quoted) # handle case where custom payload have more than one values if isinstance(current_fuzzable_values, list): values = current_fuzzable_values else: values = [current_fuzzable_values] except primitives.CandidateValueException: - _raise_dict_err(primitive_type, default_val) + _raise_dict_err(primitive_type, field_name) except Exception as err: - _handle_exception(primitive_type, default_val, err) + _handle_exception(primitive_type, field_name, err) # Handle custom (user defined) static payload on header (Adds \r\n) elif primitive_type == primitives.CUSTOM_PAYLOAD_HEADER: try: current_fuzzable_values = candidate_values_pool.\ - get_candidate_values(primitive_type, request_id=self._request_id, tag=default_val, quoted=quoted) + get_candidate_values(primitive_type, request_id=self._request_id, tag=field_name, quoted=quoted) # handle case where custom payload have more than one values if isinstance(current_fuzzable_values, list): values = current_fuzzable_values else: values = [current_fuzzable_values] except primitives.CandidateValueException: - _raise_dict_err(primitive_type, default_val) + _raise_dict_err(primitive_type, field_name) except Exception as err: - _handle_exception(primitive_type, default_val, err) + _handle_exception(primitive_type, field_name, err) # Handle custom (user defined) static payload with uuid4 suffix elif primitive_type == primitives.CUSTOM_PAYLOAD_UUID4_SUFFIX: try: current_fuzzable_value = candidate_values_pool.\ - get_candidate_values(primitive_type, request_id=self._request_id, tag=default_val, quoted=quoted) + get_candidate_values(primitive_type, request_id=self._request_id, tag=field_name, quoted=quoted) values = [primitives.restler_custom_payload_uuid4_suffix(current_fuzzable_value)] except primitives.CandidateValueException: - _raise_dict_err(primitive_type, default_val) + _raise_dict_err(primitive_type, field_name) except Exception as err: - _handle_exception(primitive_type, default_val, err) + _handle_exception(primitive_type, field_name, err) elif primitive_type == primitives.REFRESHABLE_AUTHENTICATION_TOKEN: values = [primitives.restler_refreshable_authentication_token] # Handle all the rest @@ -662,6 +689,17 @@ class Request(object): if len(values) == 0: _raise_dict_err(primitive_type, current_fuzzable_tag) + + # When testing all combinations, update tracked parameters. + if Settings().fuzzing_mode == 'test-all-combinations': + param_idx = len(fuzzable) + # Only track the parameter if there are multiple values being combined + if len(values) > 1: + if not field_name: + field_name = "tracked_param" + field_name = f"{field_name}_{param_idx}" + tracked_parameters[field_name] = param_idx + fuzzable.append(values) # lazy generation of pool for candidate values @@ -678,8 +716,14 @@ class Request(object): for ind, values in enumerate(combinations_pool): values = list(values) values = request_utilities.resolve_dynamic_primitives(values, candidate_values_pool) + + tracked_parameter_values = {} + for (k, idx) in tracked_parameters.items(): + tracked_parameter_values[k] = values[idx] + + rendered_data = "".join(values) - yield rendered_data, parser + yield rendered_data, parser, tracked_parameter_values def render_current(self, candidate_values_pool, preprocessing=False): """ Renders the next combination for the current request. @@ -690,7 +734,7 @@ class Request(object): @type preprocessing: Bool @return: (rendered request's payload, response's parser function) - @rtype : (Str, Function Pointer) + @rtype : (Str, Function Pointer, List[Str]) """ return next(self.render_iter(candidate_values_pool, diff --git a/restler/engine/core/sequences.py b/restler/engine/core/sequences.py index 77dd708..c2702d0 100644 --- a/restler/engine/core/sequences.py +++ b/restler/engine/core/sequences.py @@ -323,7 +323,8 @@ class Sequence(object): CUSTOM_LOGGING(self, candidate_values_pool) self._sent_request_data_list = [] - for rendered_data, parser in\ + + for rendered_data, parser, tracked_parameters in\ request.render_iter(candidate_values_pool, skip=request._current_combination_id, preprocessing=preprocessing): @@ -349,15 +350,18 @@ class Sequence(object): dependencies.reset_tlb() sequence_failed = False + request._tracked_parameters = tracked_parameters # Step A: Static template rendering # Render last known valid combination of primitive type values # for every request until the last for i in range(len(self.requests) - 1): prev_request = self.requests[i] - prev_rendered_data, prev_parser =\ + prev_rendered_data, prev_parser, tracked_parameters =\ prev_request.render_current(candidate_values_pool, preprocessing=preprocessing) + request._tracked_parameters.update(tracked_parameters) + # substitute reference placeholders with resolved values if not Settings().ignore_dependencies: prev_rendered_data =\ diff --git a/restler/engine/dependencies.py b/restler/engine/dependencies.py index caeca3f..c80768b 100644 --- a/restler/engine/dependencies.py +++ b/restler/engine/dependencies.py @@ -392,7 +392,7 @@ class GarbageCollectorThread(threading.Thread): # Iterate in reverse to give priority to newest resources for value in reversed(self.overflowing[type]): - rendered_data, _ = destructor.\ + rendered_data, _ , _ = destructor.\ render_current(self.req_collection.candidate_values_pool) # replace dynamic parameters diff --git a/restler/engine/fuzzing_parameters/body_schema.py b/restler/engine/fuzzing_parameters/body_schema.py index 2d87faa..624d59f 100644 --- a/restler/engine/fuzzing_parameters/body_schema.py +++ b/restler/engine/fuzzing_parameters/body_schema.py @@ -186,11 +186,25 @@ class BodySchema(): acc = '' sets = [] - for block in blocks: - primitive_type = block[0] - value = block[1] - if len(block) > 2: - quoted = block[2] + for request_block in blocks: + primitive_type = request_block[0] + if primitive_type == primitives.FUZZABLE_GROUP: + field_name = request_block[1] + value = request_block[2] + quoted = request_block[3] + examples = request_block[4] + elif primitive_type in [ primitives.CUSTOM_PAYLOAD, + primitives.CUSTOM_PAYLOAD_HEADER, + primitives.CUSTOM_PAYLOAD_UUID4_SUFFIX ]: + field_name = request_block[1] + quoted = request_block[2] + examples = request_block[3] + value = None + else: + field_name = None + value = request_block[1] + quoted = request_block[2] + examples = request_block[3] # accumulate if primitive_type == primitives.STATIC_STRING: diff --git a/restler/engine/primitives.py b/restler/engine/primitives.py index 37664e9..90228de 100644 --- a/restler/engine/primitives.py +++ b/restler/engine/primitives.py @@ -523,6 +523,7 @@ def restler_fuzzable_group(*args, **kwargs): @rtype : Tuple """ + field_name = args[0] try: enum_vals = args[1] except IndexError: @@ -535,7 +536,7 @@ def restler_fuzzable_group(*args, **kwargs): examples=None if EXAMPLES_ARG in kwargs: examples = kwargs[EXAMPLES_ARG] - return sys._getframe().f_code.co_name, enum_vals, quoted, examples + return sys._getframe().f_code.co_name, field_name, enum_vals, quoted, examples def restler_fuzzable_uuid4(*args, **kwargs): diff --git a/restler/unit_tests/test_basic_functionality_end_to_end.py b/restler/unit_tests/test_basic_functionality_end_to_end.py index 60a1c26..04de257 100644 --- a/restler/unit_tests/test_basic_functionality_end_to_end.py +++ b/restler/unit_tests/test_basic_functionality_end_to_end.py @@ -348,7 +348,7 @@ class FunctionalityTests(unittest.TestCase): try: result.check_returncode() except subprocess.CalledProcessError: - self.fail(f"Restler returned non-zero exit code: {result.returncode}") + self.fail(f"Restler returned non-zero exit code: {result.returncode} {result.stdout}") experiments_dir = self.get_experiments_dir() diff --git a/restler/utils/logger.py b/restler/utils/logger.py index aa6783e..1d77270 100644 --- a/restler/utils/logger.py +++ b/restler/utils/logger.py @@ -135,7 +135,7 @@ class SpecCoverageLog(object): SpecCoverageLog.__instance = self - def _get_request_coverage_summary_stats(self, rendered_request, req_hash): + def _get_request_coverage_summary_stats(self, rendered_request, req_hash, log_tracked_parameters=False): """ Constructs a json object with the coverage information for a request from the rendered request. This info will be reported in a spec coverage file. @@ -181,6 +181,11 @@ class SpecCoverageLog(object): req_spec['request_order'] = req.stats.request_order req_spec['sample_request'] = vars(req.stats.sample_request) + if log_tracked_parameters: + req_spec['tracked_parameters'] = {} + for k, v in req.stats.tracked_parameters.items(): + req_spec['tracked_parameters'][k] = v + return coverage_data def log_request_coverage_incremental(self, request=None, rendered_sequence=None, log_rendered_hash=True): @@ -222,7 +227,7 @@ class SpecCoverageLog(object): write_to_main("ERROR: spec coverage is being logged twice for the same rendering.", True) return - req_coverage = self._get_request_coverage_summary_stats(req, req_hash) + req_coverage = self._get_request_coverage_summary_stats(req, req_hash, log_tracked_parameters=log_rendered_hash) self._renderings_logged[req_hash] = req_coverage[req_hash]['valid'] coverage_as_json = json.dumps(req_coverage, indent=4) @@ -463,9 +468,23 @@ def custom_network_logging(sequence, candidate_values_pool, **kwargs): f"{request._current_combination_id} / {request.num_combinations(candidate_values_pool)})") for request_block in definition: primitive = request_block[0] - default_val = request_block[1] - quoted = request_block[2] - examples = request_block[3] + if primitive == primitives.FUZZABLE_GROUP: + field_name = request_block[1] + default_val = request_block[2] + quoted = request_block[3] + examples = request_block[4] + elif primitive in [ primitives.CUSTOM_PAYLOAD, + primitives.CUSTOM_PAYLOAD_HEADER, + primitives.CUSTOM_PAYLOAD_UUID4_SUFFIX ]: + field_name = request_block[1] + quoted = request_block[2] + examples = request_block[3] + else: + field_name = None + default_val = request_block[1] + quoted = request_block[2] + examples = request_block[3] + # Handling dynamic primitives that need fresh rendering every time if primitive == "restler_fuzzable_uuid4": values = [primitives.restler_fuzzable_uuid4] @@ -478,7 +497,7 @@ def custom_network_logging(sequence, candidate_values_pool, **kwargs): default_val = '_OMITTED_BINARY_DATA_' # Handle custom payload elif primitive == "restler_custom_payload_header": - current_fuzzable_tag = default_val + current_fuzzable_tag = field_name values = candidate_values_pool.get_candidate_values(primitive, request_id=request.request_id, tag=current_fuzzable_tag, quoted=quoted) if not isinstance(values, list): values = [values] @@ -486,7 +505,7 @@ def custom_network_logging(sequence, candidate_values_pool, **kwargs): default_val = values[0] # Handle custom payload elif primitive == "restler_custom_payload": - current_fuzzable_tag = default_val + current_fuzzable_tag = field_name values = candidate_values_pool.get_candidate_values(primitive, request_id=request.request_id, tag=current_fuzzable_tag, quoted=quoted) if not isinstance(values, list): values = [values] @@ -494,7 +513,7 @@ def custom_network_logging(sequence, candidate_values_pool, **kwargs): default_val = values[0] # Handle custom payload with uuid4 suffix elif primitive == "restler_custom_payload_uuid4_suffix": - current_fuzzable_tag = default_val + current_fuzzable_tag = field_name values = candidate_values_pool.get_candidate_values(primitive, request_id=request.request_id, tag=current_fuzzable_tag, quoted=quoted) default_val = values[0] # Handle all the rest diff --git a/src/compiler/Restler.Compiler/CodeGenerator.fs b/src/compiler/Restler.Compiler/CodeGenerator.fs index c7942d8..8e1e4f1 100644 --- a/src/compiler/Restler.Compiler/CodeGenerator.fs +++ b/src/compiler/Restler.Compiler/CodeGenerator.fs @@ -74,14 +74,14 @@ let rec getRestlerPythonPayload (payload:FuzzingPayload) (isQuoted:bool) : Reque | Number -> Restler_fuzzable_number { defaultValue = v ; isQuoted = false ; exampleValue = exv } | Uuid -> Restler_fuzzable_uuid4 { defaultValue = v ; isQuoted = isQuoted ; exampleValue = exv } - | PrimitiveType.Enum (_, enumeration, defaultValue) -> - // TODO: should this be generating unique fuzzable group tags? Why is one needed? + | PrimitiveType.Enum (enumPropertyName, _, enumeration, defaultValue) -> let defaultStr = match defaultValue with | Some v -> sprintf ", default_enum=\"%s\"" v | None -> "" let groupValue = - (sprintf "\"fuzzable_group_tag\", [%s] %s " + (sprintf "\"%s\", [%s] %s " + enumPropertyName (enumeration |> List.map (fun s -> sprintf "'%s'" s) |> String.concat ",") defaultStr ) @@ -296,7 +296,7 @@ let generatePythonParameter includeOptionalParameters parameterKind (requestPara | PrimitiveType.DateTime | PrimitiveType.Uuid -> true - | PrimitiveType.Enum (enumType, _, _) -> + | PrimitiveType.Enum (_, enumType, _, _) -> isPrimitiveTypeQuoted enumType isNullValue | PrimitiveType.Object | PrimitiveType.Int diff --git a/src/compiler/Restler.Compiler/Compiler.fs b/src/compiler/Restler.Compiler/Compiler.fs index a350308..4dbef83 100644 --- a/src/compiler/Restler.Compiler/Compiler.fs +++ b/src/compiler/Restler.Compiler/Compiler.fs @@ -290,6 +290,19 @@ module private Parameters = let parameterPayload = generateGrammarElementForSchema p.ActualSchema (specExampleValue, true) [] id + // Add the name to the parameter payload + let parameterPayload = + match parameterPayload with + | LeafNode leafProperty -> + let leafNodePayload = + match leafProperty.payload with + | Fuzzable (Enum(propertyName, propertyType, values, defaultValue), x, y) -> + Fuzzable (Enum(p.Name, propertyType, values, defaultValue), x, y) + | _ -> leafProperty.payload + LeafNode { leafProperty with payload = leafNodePayload } + | InternalNode (internalNode, children) -> + // TODO: need enum test to see if body enum is fine. + parameterPayload { name = p.Name payload = parameterPayload diff --git a/src/compiler/Restler.Compiler/Grammar.fs b/src/compiler/Restler.Compiler/Grammar.fs index 4077bca..c346a68 100644 --- a/src/compiler/Restler.Compiler/Grammar.fs +++ b/src/compiler/Restler.Compiler/Grammar.fs @@ -102,7 +102,8 @@ type PrimitiveType = | DateTime /// The enum type specifies the list of possible enum values /// and the default value, if specified. - | Enum of PrimitiveType * string list * string option + /// (tag, data type, possible values, default value if present) + | Enum of string * PrimitiveType * string list * string option type NestedType = | Array diff --git a/src/compiler/Restler.Compiler/SwaggerVisitors.fs b/src/compiler/Restler.Compiler/SwaggerVisitors.fs index 48ad901..9d28a36 100644 --- a/src/compiler/Restler.Compiler/SwaggerVisitors.fs +++ b/src/compiler/Restler.Compiler/SwaggerVisitors.fs @@ -145,7 +145,7 @@ module SwaggerVisitors = match enumValues with | [] -> "null" | h::rest -> h - Fuzzable (PrimitiveType.Enum (grammarPrimitiveType, enumValues, defaultValue), defaultFuzzableEnumValue, exv) + Fuzzable (PrimitiveType.Enum (propertyName, grammarPrimitiveType, enumValues, defaultValue), defaultFuzzableEnumValue, exv) | NJsonSchema.JsonObjectType.Object | NJsonSchema.JsonObjectType.None -> // Example of JsonObjectType.None: "content": {} without a type specified in Swagger. @@ -214,7 +214,7 @@ module SwaggerVisitors = | _ when v.Type = JTokenType.Null -> null | PrimitiveType.String | PrimitiveType.DateTime - | PrimitiveType.Enum (PrimitiveType.String, _, _) + | PrimitiveType.Enum (_, PrimitiveType.String, _, _) | PrimitiveType.Uuid -> // Remove the start and end quotes, which are preserved with 'Formatting.None'. if rawValue.Length > 1 then